Освоив хуки, многие React-разработчики испытали эйфорию, получив наконец простой и удобный инструментарий, позволяющий реализовывать задачи значительно меньшим количеством кода.
Но означает ли это, что стандартные перехватчики useState и useReducer, поставляемые «из коробки», — это все, что нам нужно для управления состоянием? На мой взгляд, в сыром виде их использовать не очень удобно; их скорее можно рассматривать как основу для создания действительно удобных механизмов управления состоянием.
Сами разработчики React настоятельно рекомендуют разработку собственных хуков, так почему бы не сделать это? Под катом мы на очень простом и понятном примере рассмотрим, что не так с обычными крючками и как их можно улучшить, настолько, что мы вообще отказываемся от их использования в чистом виде.
Есть некое поле для ввода, условно, имени.
И есть кнопка, нажав на которую мы должны сделать запрос к серверу с введенным именем (какой-то поиск).
Казалось бы, что может быть проще? Однако решение далеко не очевидно.
Первая наивная реализация:
Что в этом плохого? Если пользователь после ввода чего-либо в поле отправит форму дважды, у нас сработает только первый запрос, потому что при втором клике запрос не изменится и useEffect не сработает. Если представить, что наше приложение — это сервис поиска билетов, и пользователь вполне может отправлять форму снова и снова через определенные промежутки времени, не внося изменений, то такая реализация нас не устроит! Использование имени в качестве зависимости для useEffect также недопустимо, иначе форма будет отправлена немедленно при изменении текста.const App = () => { const [name, setName] = useState(''); const [request, setRequest] = useState(); const [result, setResult] = useState(); useEffect(() => { fetch('http://example.api/' + name).
then((data) => { setResult(data.result); }); }, [request]); return <div> <input onChange={e => setName(e.target.value)}/> <input type="submit" value="Check" onClick={() => setRequest(name)}/> { result && <div>Result: { result }</div> } </div>; }
Что ж, вам придется проявить творческий подход.
const App = () => {
const [name, setName] = useState('');
const [request, setRequest] = useState();
const [result, setResult] = useState();
useEffect(() => {
fetch('http://example.api/' + name).
then((data) => {
setResult(data.result);
});
}, [request]);
return <div>
<input onChange={e => setName(e.target.value)}/>
<input type="submit" value="Check" onClick={() => setRequest(!request)}/>
{ result && <div>Result: { result }</div> }
</div>;
}
Теперь при каждом нажатии мы будем менять значение запроса на противоположное, и этим добиться желаемого поведения.
Это очень маленький и невинный костыль, но он делает код несколько запутанным для понимания.
Возможно, сейчас вам кажется, что я раздуваю проблему и раздуваю ее масштабы.
Что ж, чтобы ответить, правда это или нет, нужно сравнить этот код с другими реализациями, предлагающими более выразительный подход. Давайте рассмотрим этот пример на теоретическом уровне, используя абстракцию потока.
Это очень удобно для описания состояния пользовательских интерфейсов.
Итак, у нас есть два потока: данные, вводимые в текстовое поле (name$), и поток кликов по кнопке отправки формы (click$).
Из них нам нужно создать третий, объединенный поток запросов к серверу.
name$ __(C)____(Ca)_____(Car)____________________(Carl)___________
click$ ___________________________()______()________________()_____
request$ ___________________________(Car)___(Car)_____________(Carl)_
Это то поведение, которого нам нужно достичь.
Каждый поток имеет два аспекта: значение, которое он имеет, и момент времени, в который значения протекают через него.
В разных ситуациях нам может понадобиться тот или иной аспект, или оба сразу.
Вы можете сравнить это с ритмом и гармонией в музыке.
Потоки, для которых важно только время их работы, также называются сигналами.
В нашем случае click$ — это чистый сигнал: не имеет значения, какое значение через него проходит (неопределенное/true/Event/что угодно), важно только то, когда это происходит. Название дела$ наоборот: ее изменения никоим образом не влекут за собой никаких изменений в системе, но сам ее смысл может нам понадобиться в определенный момент. И из этих двух потоков нам нужно сделать третий, взяв время из первого и ценность из второго.
В случае с Rxjs у нас для этого есть почти готовый оператор: const names$ = fromEvent(.
); const click$ = fromEvent(.
); const request$ = click$.
pipe(withLatestFrom(name$), map(([name]) => fromPromise(fetch(.
))));
Однако практическое использование Rx в React может быть весьма неудобным.
Лучший вариант — библиотека мистер , построенный на тех же функционально-реактивных принципах, что и Rx, но специально адаптированный для использования с React по принципу «полной реактивности» и подключаемый как хук.
import useMrr from 'mrr/hooks';
const App = props => {
const [state, set] = useMrr(props, {
result: [name => fetch('http://example.api/' + name).
then(data => data.result), '-name', 'submit'],
});
return <div>
<input value={state.name} onChange={set('name')}/>
<input type="submit" value="Check" onClick={set('submit')}/>
{ state.result && <div>Result: { state.result }</div> }
</div>;
}
Интерфейс useMrr похож на useState или useReducer: он возвращает объект состояния (значения всех потоков) и сеттер для помещения значений в потоки.
Но внутри все немного иначе: каждое поле состояния (=thread), кроме тех, в которые мы помещаем значения непосредственно из DOM-событий, описывается функцией и списком родительских потоков, изменение которых вызовет дочерний подлежит перерасчету.
В этом случае в функцию будут подставлены значения родительских потоков.
Если мы хотим просто получить значение потока, но не реагировать на его изменение, то перед именем пишем «минус», как и в случае с именем.
Мы получили желаемое поведение, по сути, одной строкой.
Но дело не только в краткости.
Сравним полученные результаты более подробно, и в первую очередь по таким параметрам, как читаемость и понятность получаемого кода.
В mrr вы получаете практически полное отделение «логики» от «шаблона»: в JSX не нужно писать никаких сложных императивных обработчиков.
Все предельно декларативно: мы просто сопоставляем DOM-событие с соответствующим потоком, практически без преобразований (для полей ввода автоматически извлекается значение e.target.value, если не указано иное), и уже в структуре useMrr описываем, как базовые потоки формируются дочерними.
Таким образом, как в случае синхронного, так и асинхронного преобразования данных, мы всегда можем легко проследить, как формируется наше значение.
По сравнению с Px: нам даже не пришлось использовать дополнительные операторы: если в результате выполнения функций mrr он получит промис, он автоматически дождется его разрешения и поместит полученные данные в поток.
Также вместо оператора withLatestFrom мы использовали пассивное слушание (знак минус), что более удобно.
Давайте представим, что помимо имени нам нужно будет отправить и другие поля.
Затем в mrr добавим еще один поток пассивного прослушивания: result: [(name, surname) => fetch(.
), '-name', '-surname', 'submit'],
А в Rx придётся создавать ещё один withLatestFrom с картой, либо сначала объединять имя и фамилию в один поток.
Но вернемся к хукам и мрр.
Более читаемая запись о зависимостях, которая всегда показывает, как генерируются данные, является, пожалуй, одним из главных преимуществ.
Текущий интерфейс useEffect принципиально не позволяет реагировать на потоки сигналов, поэтому вам придется придумывать разные трюки.
Другой момент заключается в том, что вариант обычных хуков влечет за собой дополнительные рендеринги.
Если пользователь просто нажимает на кнопку, это не влечет за собой никаких изменений в пользовательском интерфейсе, который React должен отрисовать.
Однако рендеринг будет вызван.
В опции mrr возвращаемое состояние будет обновляться только тогда, когда ответ от сервера уже поступил.
— Экономия на спичках, говорите? Ну, возможно.
Но лично у меня сам принцип «в любой непонятной ситуации перерендерить», который лежит в основе базовых хуков, вызывает отвращение.
Дополнительные рендеры также означают новое формирование обработчиков событий.
Кстати, с обычными крючками здесь все плохо.
Обработчики не только обязательны, но их также необходимо перегенерировать при каждом рендеринге.
И полноценно использовать кеширование здесь не получится, потому что.
многие обработчики должны быть зациклены во внутренних переменных компонента.
Обработчики mrr более декларативны, и mrr уже имеет встроенное кэширование: set('name') будет сгенерирован только один раз и будет подставлен из кеша при последующих рендерах.
По мере увеличения кодовой базы императивные обработчики могут стать еще более громоздкими.
Допустим, нам также нужно показать количество отправленных пользователем форм.
const App = () => {
const [request, makeRequest] = useState();
const [name, setName] = useState('');
const [result, setResult] = useState(false);
const [clicks, setClicks] = useState(0);
useEffect(() => {
fetch('http://example.api/' + name).
then((data) => {
setResult(data.result);
});
}, [request]);
return <div>
<input onChange={e => setName(e.target.value)}/>
<input type="submit" value="Check" onClick={() => {
makeRequest(!request);
setClicks(clicks + 1);
}}/><br />
Clicked: { clicks }
</div>;
}
Это выглядит не очень красиво.
Можно, конечно, сделать обработчик отдельной функцией внутри компонента.
Читабельность повысится, но проблема регенерации функции при каждом рендере останется, как и проблема императивности.
По сути, это всего лишь процедурный код, несмотря на распространенное мнение, что React API постепенно меняется в сторону функционального подхода.
Тем, кому масштаб проблемы покажется преувеличенным, могу ответить, что, например, разработчики React сами осознают проблему ненужной генерации обработчиков, сразу же услужливо предложив нам костыль в виде useCallback. На мрр:
const App = props => {
const [state, set] = useMrr(props, {
$init: {
clicks: 0,
},
isValid: [name => fetch('http://example.api/' + name).
then(data => data.isValid), '-name', 'makeRequest'],
clicks: [a => a + 1, '-clicks', 'makeRequest'],
});
return <div>
<input onChange={set('name')}/>
<input type="submit" value="Check" onClick={set('makeRequest')}/>
</div>;
}
Более удобная альтернатива — useReducer, позволяющая отказаться от императивности обработчиков.
Но остаются другие важные проблемы: отсутствие работы с сигналами (поскольку за побочные эффекты будет отвечать тот же useEffect), а также худшая читаемость при асинхронных преобразованиях (иными словами, сложнее проследить взаимосвязь между полями store из-за того же useEffect ).
Если в mrr график зависимостей между полями состояний (потоками) сразу хорошо виден, то в хуках придется немного побегать глазами вверх-вниз.
Также использовать useState и useReducer вместе в одном компоненте не очень удобно (опять же будут сложные императивные обработчики, которые будут что-то менять в useState и отправить действие), поэтому, скорее всего, вам нужно будет принять тот или иной вариант перед разработкой компонента.
Конечно, рассмотрение всех аспектов можно продолжать и продолжать.
Чтобы не выходить за рамки статьи, попутно коснусь некоторых менее важных моментов.
Централизованное логирование, отладка.
Так как в mrr все потоки содержатся в одном хабе, то для отладки достаточно добавить один флаг: const App = props => {
const [state, set] = useMrr(props, {
$log: true,
$init: {
clicks: 0,
},
isValid: [name => fetch('http://example.api/' + name).
then(data => data.isValid), '-name', 'makeRequest'], clicks: [a => a + 1, '-clicks', 'makeRequest'], }); .
После чего все изменения тредов будут отображаться в консоли.
Для доступа ко всему состоянию (т.е.
текущим значениям всех потоков) существует псевдопоток $state: a: [({ name, click, result }) => { .
}, '$state', 'click'],
Итак, при необходимости или если вы очень привыкли к стилю redux, вы можете написать в стиле redax на mrr, возвращая новое значение поля на основе события и всего предыдущего состояния.
А вот наоборот (писать на useReducer или redax в стиле mrr) не получится, ввиду отсутствия в них реактивности.
Работа со временем.
Помните два аспекта потоков: смысл и время, гармонию и ритм? Итак, с первым в обычных хуках работать довольно просто и удобно, а со вторым – нет. Под работой со временем я подразумеваю формирование дочерних потоков, «ритм» которых отличается от родительского.
Это, в первую очередь, разного рода фильтры, дебауны, дроссели и т. д. Все это вам, скорее всего, придется реализовывать самостоятельно.
В mrr можно использовать готовые операторы из коробки.
Джентльменский набор mrr уступает разновидности операторов Rx, но имеет более интуитивное именование.
Межкомпонентное взаимодействие.
Помню, в Redax считалось хорошей практикой создавать только один магазин.
Если мы используем useReducer во многих компонентах, Может возникнуть проблема с организацией взаимодействия между магазинами.
На mrr потоки могут свободно «перетекать» из одного компонента в другой, как вверх, так и вниз по иерархии, но это не создаст проблем из-за декларативности подхода.
Подробно
эта тема, а также другие возможности mrr API описаны в статье Актеры+FRP в React
выводы
Новые хуки React великолепны и делают нашу жизнь проще, но у них есть некоторые недостатки, которые можно было бы устранить с помощью хуков общего назначения более высокого уровня (управление состоянием).UseMrr из функционально-реактивной библиотеки mrr был предложен и рассмотрен как таковой.
Проблемы и их решения:
- ненужные пересчеты данных для каждого рендеринга (отсутствуют в mrr благодаря реактивности на основе push)
- ненужные рендеринги, когда изменение состояния не влечет за собой изменение пользовательского интерфейса
- плохая читаемость кода с асинхронными преобразованиями (по сравнению с синхронными).
В мрр асинхронный код не уступает синхронному по читабельности и выразительности.
Большинство проблем, обсуждаемых в недавняя статья об useEffect , на мрр в принципе невозможно
- императивные обработчики, не всегда кэшируемые (в mrr кэшируются автоматически, почти всегда можно кэшировать, декларативные)
- одновременное использование useState и useReducer может создать неуклюжий код.
- отсутствие инструментов для преобразования потоков во времени (debounce, throttle, Race Condition)
А ведь именно это и предлагается, только вместо разрозненных реализаций для каждой отдельной задачи предлагается целостное, последовательное решение.
Многие проблемы стали нам слишком знакомы, чтобы их можно было ясно понять.
Например, асинхронные преобразования всегда выглядели сложнее и запутаннее синхронных, и хуки в этом смысле не хуже более ранних подходов (edax и т.п.
).
Чтобы признать это проблемой, вы должны сначала увидеть другие подходы, предлагающие лучшее решение.
Целью данной статьи не является навязывание какой-либо конкретной точки зрения, а скорее привлечение внимания к проблеме.
Я уверен, что существуют или сейчас создаются другие решения, которые могут стать достойной альтернативой, но пока не получили широкой известности.
Предстоящий React Cache API также может существенно изменить ситуацию.
Буду рад покритиковать и обсудить в комментариях.
Желающие также могут посмотреть выступление на эту тему на сайте киев 28 марта.
Теги: #JavaScript #react #rx #FRP #mrr
-
Великие Изобретения: Интернет
19 Oct, 24 -
Обновление Yota Access На Модеме
19 Oct, 24 -
Предотвращение Sql-Инъекций
19 Oct, 24 -
Сапсан Взлетел - Opera 10 Альфа
19 Oct, 24 -
Удобство Использования Списков Элементов
19 Oct, 24