Компоненты и хуки должны быть чистыми¶
Чистые функции выполняют только вычисления и ничего больше. Это облегчает понимание и отладку кода, а также позволяет React автоматически оптимизировать компоненты и хуки.
Чистота компонентов
Эта справочная страница охватывает расширенные темы и требует знакомства с концепциями, рассмотренными на странице «Чистота компонентов».
Почему чистота имеет значение?¶
Одна из ключевых концепций, которая делает React React, - это чистота. Чистый компонент или хук - это тот, который:
- Идемпотентен - Вы всегда получаете один и тот же результат каждый раз, когда запускаете его с теми же входными данными - props, state, context для компонентных входов; и аргументы для входов хука.
- Не имеет побочных эффектов в рендере - Код с побочными эффектами должен выполняться отдельно от рендеринга. Например, как обработчик событий - когда пользователь взаимодействует с пользовательским интерфейсом и заставляет его обновляться; или как эффект - который запускается после рендера.
- Не мутирует нелокальные значения: Компоненты и хуки должны никогда не изменять значения, которые не создаются локально при рендере.
Когда рендер остается чистым, React может понять, как расставить приоритеты, какие обновления наиболее важны для пользователя, чтобы увидеть их первыми. Это становится возможным благодаря чистоте рендеринга: поскольку компоненты не имеют побочных эффектов в рендеринге, React может приостановить рендеринг компонентов, которые не так важно обновлять, и вернуться к ним позже, когда это будет необходимо.
Конкретно это означает, что логика рендеринга может быть запущена несколько раз, что позволяет React обеспечить пользователю приятный пользовательский опыт. Однако если у вашего компонента есть неотслеживаемый побочный эффект - например, изменение значения глобальной переменной во время рендеринга - когда React снова запустит ваш код рендеринга, ваши побочные эффекты будут вызваны не так, как вы хотели. Это часто приводит к неожиданным ошибкам, которые могут ухудшить восприятие приложения пользователями. Вы можете увидеть пример этого на странице Keeping Components Pure.
Как React выполняет ваш код?¶
React является декларативным: вы указываете React, что именно нужно отобразить, а React сам решает, как лучше показать это пользователю. Для этого у React есть несколько этапов, на которых он выполняет ваш код. Вам не нужно знать обо всех этих фазах, чтобы хорошо использовать React. Но на высоком уровне вы должны знать, какой код выполняется в render, а какой - за его пределами.
Под рендерингом понимается вычисление того, как должна выглядеть следующая версия вашего пользовательского интерфейса. После рендеринга эффекты промываются (то есть выполняются до тех пор, пока их не останется) и могут обновить расчеты, если эффекты влияют на макет. React берет этот новый расчет и сравнивает его с расчетом, использованным для создания предыдущей версии вашего пользовательского интерфейса, а затем фиксирует только минимальные изменения, необходимые для DOM (то, что пользователь видит на самом деле), чтобы привести его в соответствие с последней версией.
Как определить, выполняется ли код при рендеринге
Одна из быстрых эвристик, позволяющих определить, выполняется ли код во время рендеринга, - это посмотреть, где он находится: если он написан на верхнем уровне, как в примере ниже, то велика вероятность, что он выполняется во время рендеринга.
1 2 3 4 |
|
Обработчики событий и эффекты не запускаются при рендеринге:
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 6 7 |
|
Компоненты и хуки должны быть идемпотентными¶
Компоненты должны всегда возвращать один и тот же результат по отношению к своим входным данным - реквизитам, состоянию и контексту. Это известно как идемпотентность. Идемпотентность - термин, получивший распространение в функциональном программировании. Он относится к идее, что вы всегда получаете один и тот же результат каждый раз, когда запускаете этот кусок кода с одними и теми же входными данными.
Это означает, что весь код, который выполняется во время рендеринга, должен быть идемпотентным, чтобы это правило выполнялось. Например, эта строка кода не является идемпотентной (и, следовательно, компонент тоже):
1 2 3 4 |
|
Функция new Date()
не является идемпотентной, поскольку всегда возвращает текущую дату и меняет свой результат при каждом вызове. При рендеринге вышеуказанного компонента время, отображаемое на экране, будет зациклено на том времени, когда компонент был рендерирован. Аналогично, функции вроде Math.random()
также не являются идемпотентными, поскольку при каждом вызове они возвращают разные результаты, даже если входные данные одинаковы.
Это не означает, что вы не должны использовать неидемпотентные функции вроде new Date()
вообще - вы просто должны избегать их использования во время рендеринга. В данном случае мы можем синхронизировать последнюю дату для этого компонента с помощью Effect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Обернув неэдемпотентный вызов new Date()
в Effect, он переносит это вычисление за пределы рендеринга.
Если вам не нужно синхронизировать внешнее состояние с React, вы также можете рассмотреть возможность использования обработчика событий, если оно должно обновляться только в ответ на взаимодействие с пользователем.
Побочные эффекты должны выполняться вне рендера¶
Побочные эффекты не должны запускаться в рендере, так как React может рендерить компоненты несколько раз, чтобы создать наилучший пользовательский опыт.
Побочные эффекты
Побочные эффекты - это более широкое понятие, чем эффекты. Эффекты относятся к коду, обернутому в useEffect
, в то время как побочный эффект - это общий термин для кода, который имеет любой наблюдаемый эффект, кроме его основного результата - возврата значения вызывающей стороне.
Побочные эффекты обычно пишутся внутри обработчиков событий или Эффектов. Но никогда - во время рендеринга.
Хотя рендеринг должен быть чистым, побочные эффекты необходимы, чтобы в какой-то момент ваше приложение сделало что-нибудь интересное, например показало что-то на экране! Ключевой момент этого правила заключается в том, что побочные эффекты не должны выполняться в рендере, так как React может рендерить компоненты несколько раз. В большинстве случаев для обработки побочных эффектов вы будете использовать обработчики событий. Использование обработчика событий явно говорит React, что этот код не нужно запускать во время рендеринга, сохраняя чистоту рендеринга. Если вы исчерпали все варианты - и только в крайнем случае - вы также можете обрабатывать побочные эффекты с помощью useEffect
.
Когда можно использовать мутацию?¶
Локальная мутация¶
Одним из распространенных примеров побочного эффекта является мутация, которая в JavaScript означает изменение значения не примитивного значения. В целом, хотя мутация не является идиоматической в React, локальная мутация абсолютно нормальна:
1 2 3 4 5 6 7 8 9 10 |
|
Нет необходимости искажать код, чтобы избежать локальной мутации. Для краткости здесь можно было бы использовать и Array.map
, но нет ничего плохого в том, чтобы создать локальный массив и затем заталкивать в него элементы во время рендеринга.
Несмотря на то, что выглядит так, будто мы мутируем items
, важно отметить, что этот код делает это только локально - мутация не «запоминается» при повторном рендеринге компонента. Другими словами, items
будет существовать только до тех пор, пока существует компонент. Поскольку items
всегда восстанавливается при каждом рендеринге <FriendList />
, компонент всегда будет возвращать один и тот же результат.
С другой стороны, если items
был создан вне компонента, он сохраняет свои предыдущие значения и запоминает изменения:
1 2 3 4 5 6 7 8 9 10 |
|
Когда <FriendList />
будет запущен снова, мы продолжим добавлять friends
к items
при каждом запуске этого компонента, что приведет к многочисленным дублирующимся результатам. Эта версия <FriendList />
имеет наблюдаемые побочные эффекты во время рендеринга и нарушает правило.
Ленивая инициализация¶
Ленивая инициализация также подходит, несмотря на то, что не является полностью «чистой»:
1 2 3 4 5 |
|
Изменение DOM¶
В логике рендеринга компонентов React не допускаются побочные эффекты, которые видны непосредственно пользователю. Другими словами, простой вызов функции компонента сам по себе не должен приводить к изменениям на экране.
1 2 3 |
|
Один из способов добиться желаемого результата обновления window.title
вне рендеринга - это синхронизировать компонент с window
.
Пока вызов компонента несколько раз безопасен и не влияет на рендеринг других компонентов, React не волнует, является ли он на 100% чистым в строгом смысле функционального программирования. Важнее, что компоненты должны быть идемпотентными.
Пропсы и состояние неизменяемы¶
Пропсы и состояние компонента неизменяемы snapshots. Никогда не изменяйте их напрямую. Вместо этого передавайте новые пропсы вниз и используйте функцию setter из useState
.
Можно рассматривать пропсы и значения состояния как моментальные снимки, которые обновляются после рендеринга. По этой причине вы не изменяете переменные props или state напрямую: вместо этого вы передаете новые props или используете предоставленную вам функцию setter, чтобы сообщить React, что состояние должно быть обновлено при следующем рендеринге компонента.
Не мутируйте пропсы¶
Пропсы неизменяемы, потому что если вы их измените, приложение будет выдавать непоследовательный вывод, который может быть трудно отладить, поскольку он может работать или не работать в зависимости от обстоятельств.
1 2 3 4 |
|
1 2 3 4 |
|
Не мутируйте состояние¶
useState
возвращает переменную state и сеттер для обновления этого состояния.
1 |
|
Вместо того чтобы обновлять переменную state на месте, нам нужно обновить ее с помощью функции setter, которую возвращает useState
. Изменение значения переменной state не приводит к обновлению компонента, оставляя пользователей с устаревшим пользовательским интерфейсом. Использование функции setter информирует React о том, что состояние изменилось, и что нам нужно поставить в очередь повторный рендеринг для обновления пользовательского интерфейса.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Возвращаемые значения и аргументы хуков неизменяемы¶
После передачи значений в хук их нельзя изменять. Как и пропсы в JSX, значения становятся неизменяемыми при передаче хуку.
1 2 3 4 5 6 7 8 |
|
1 2 3 4 5 6 7 8 |
|
Одним из важных принципов React является локальное рассуждение: способность понять, что делает компонент или хук, глядя на его код в изоляции. К хукам следует относиться как к «черным ящикам», когда они вызываются. Например, пользовательский хук может использовать свои аргументы в качестве зависимостей для мемоизации значений внутри него:
1 2 3 4 5 6 7 8 9 10 11 |
|
Если вы измените аргументы Hooks, мемоизация пользовательского хука станет некорректной, поэтому важно избегать этого.
1 2 3 |
|
1 2 3 |
|
Аналогично, важно не изменять возвращаемые значения хуков, так как они могут быть мемоизированы.
Значения неизменяемы после передачи в JSX¶
Не мутируйте значения после того, как они были использованы в JSX. Переместите мутацию до создания JSX.
Когда вы используете JSX в выражении, React может нетерпеливо оценить JSX до того, как компонент закончит рендеринг. Это означает, что мутация значений после их передачи в JSX может привести к устаревшему пользовательскому интерфейсу, поскольку React не будет знать, что нужно обновить вывод компонента.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Источник — https://react.dev/reference/rules/components-and-hooks-must-be-pure