И почему вы не должны изменять состояние из зависимостей useEffect

Сколько раз мы хотели обновить внутреннее состояние компонента данными из его свойств? Есть множество способов сделать это, но некоторые из них имеют подводные камни.

Проблема: реквизит не всегда режут

React придерживается мнения, что не обновляет состояние для отражения изменений свойств, и не без оснований. Гораздо труднее и сложнее отслеживать, что должно отражать состояние. Это идея управляемых компонентов, когда управляемый компонент обновляет состояние родителя вместо того, чтобы пытаться синхронизировать два компонента.

Проблема с полностью управляемыми компонентами заключается в том, что иногда состояние внутри компонента может быть довольно сложным. Родителя может интересовать только, скажем, перечисление конечного результата поиска в базе данных сервера. В идеале результаты поиска должны отображаться в глобальном хранилище состояний, таком как Redux или context, и состояние просто просачивается туда, где оно необходимо в нашем приложении. Но иногда - особенно с компонентом многократного использования - было бы неплохо иметь возможность инкапсулировать компонент поиска, читать value для поиска и возвращать результаты, вызывая свойство onSearch.

Вот почему многие из нас в конечном итоге используют состояние в компоненте, который должен отслеживать изменения в получаемых свойствах.

Давайте рассмотрим некоторые способы обновления состояния на основе свойств в функциональных компонентах, их плюсы и минусы, следуя моему предпочтительному подходу.

Использование useEffect: пожалуйста, не

Как часто мы видели что-то подобное?

Это похоже на простой и элегантный подход. Каждый раз при изменении externalState срабатывает эффект, обновляя internalState. Три строчки и готово.

Только это невероятно дорого.

Каждый раз, когда externalState изменяется, мы не слышим об этом до тех пор, пока не будет сгенерирована вся DOM приложения. Затем мы вызываем setInternalState, и это заставляет приложение заново перезапускать фазу рендеринга и воссоздавать новый Структура DOM.

Ради любви к своим пользователям, пожалуйста, не делайте этого. Я пишу эту статью специально, потому что я видел это слишком много раз и не хочу стать лысым, оторвав волосы от головы.

Отслеживание предыдущей стоимости в состоянии: эффективно, не слишком дорого

React предлагает эту альтернативу для обновления состояния при появлении новых значений. Это их полуофициальная замена для getDerivedStateFromProps:

Эта альтернатива выполняет всю работу на этапе рендеринга перед согласованием DOM. Это делает его намного более эффективным, чем исходный пример useEffect.

usePrevious: Самый дешевый из них

Третья альтернатива также исходит из документации React. Они описывают это как потенциальное будущее usePrevious Hook, и это выглядит многообещающим. Если мы хотим использовать его для обновления состояния, это будет выглядеть примерно так:

В этом случае мы не используем useEffect, чтобы инициировать изменение. Поскольку мы обновляем ссылку, а не вызываем setState функцию, при обновлении previousValueRef повторная отрисовка не выполняется. Когда компонент вызывается следующим, предоставленный value сравнивается с сохраненным previousValueRef.current, и, если теперь они другие, мы обновляем internalState.

Есть два ключа к объяснению того, почему это работает:

  1. Хук useEffect не имеет массива зависимостей. Это означает, что он будет запускаться после каждого рендеринга, что позволяет постоянно обновлять usePreviousRef.
  2. Мы устанавливаем внутреннее состояние только в случае необходимости, потому что предыдущее и внутреннее значение не соответствуют предоставленному значению. Если мы проверяем только предыдущее значение, мы попадаем в бесконечный цикл рендеринга, каждый раз обновляя внутреннее состояние.

Примечание: честно говоря, это меня смущает. Несмотря на то, что в документации сказано, что setState функции не должны принудительно выполнять повторный рендеринг, если они получают то же значение, что и содержат, я попадаю в бесконечный цикл рендеринга, даже когда вызываю что-то столь же простое, как setState(false), без предварительной проверки false. Если кто-то знает, почему это происходит, я хотел бы услышать это в ответах.

Второй и третий варианты практически идентичны по производительности. Основное отличие состоит в том, что вторая альтернатива выполняет всю свою работу во время рендеринга, тогда как третья альтернатива выполняет часть своей работы после того, как компонент отображается на странице. Обновление происходит только в том случае, если компонент повторно визуализируется с новым значением, а не обновляет сразу два отдельных элемента состояния. Это мелочь, но кажется хорошей микрооптимизацией.

И да, преждевременная оптимизация - это смерть поддержки кода. В данном случае я выбираю между двумя рекомендациями, сделанными людьми из команды разработчиков React, поэтому я не теряю из-за этого много сна.

Мои личные предпочтения: useUpdatableState

Я предпочитаю третью альтернативу, потому что она кажется мне менее дорогой с точки зрения изменения состояния. Но это немного беспорядочно с точки зрения читаемости кода и не устраняет одну из моих главных жалоб: я не могу выбирать, что является необходимым изменением состояния. Он обновляется на основе строгого равенства, когда я могу обновлять свое состояние только при соблюдении определенных критериев.

Введите useUpdatableState:

import useUpdatableState from '@landisdesign/use-updatable-state';
const [state, setState, changed] = useUpdatableState(value);

По сути, это замена useState, за исключением того, что он обновляется на основе новых входящих значений. Пока входящее значение не изменяется, его возвращаемое состояние зависит от того, что передано в setState. Когда действительно приходит новое значение, state обновляется. Параметр changed сообщает мне, когда state отражает входящее значение вместо значения, которое я изменял, на случай, если мне нужно внести изменения в другое состояние или вызвать побочные эффекты.

Что еще более важно, это позволяет мне указать предикат во втором аргументе, например:

В этом примере аргумент (a, b) => a.id === b.id сообщает useUpdatableState, что если идентификаторы совпадают, мне не следует обновлять свое внутреннее состояние на основе этого внешнего значения. Это похоже на то, что делает useSelector Hook react-redux, позволяя нам выбирать, что стоит принудительно сбросить до нашего состояния.

TL; DR: не менять состояние с помощью зависимостей useEffect

Вот о чем действительно эта статья. Вот как минимум две альтернативы, которые не заставят ваших пользователей платить за попытки написать красивый, но неэффективный код. У тебя есть другие? Оставляйте их в комментариях ниже.

Спасибо за прочтение!