Сохранение «чистоты» компонентов
Некоторые функции JavaScript являются *чистыми*. Чистые функции выполняют только вычисления и ничего больше. Строго придерживаясь написания компонентов исключительно в виде чистых функций, вы сможете избежать целого ряда запутанных ошибок и непредсказуемого поведения по мере роста вашей кодовой базы. Однако для получения этих преимуществ необходимо соблюдать несколько правил.
Вы узнаете
- Что такое чистота и как она помогает избежать ошибок
- Как сохранять чистоту компонентов, не допуская изменений на этапе рендеринга
- Как использовать строгий режим (Strict Mode) для поиска ошибок в ваших компонентах
Чистота: компоненты как формулы
В информатике (и особенно в мире функционального программирования) чистая функция — это функция со следующими характеристиками:
- Она занимается только своим делом. Она не изменяет никаких объектов или переменных, существовавших до ее вызова.
- Одинаковые входные данные — одинаковый результат. При одинаковых входных данных чистая функция всегда должна возвращать один и тот же результат.
Возможно, вам уже знаком один из примеров чистых функций: формулы в математике.
Рассмотрим следующую математическую формулу: y = 2x.
Если x = 2, то y = 4. Всегда.
Если x = 3, то y = 6. Всегда.
Если x = 3, то y не будет иногда равным 9, –1 или 2,5 в зависимости от времени суток или состояния фондового рынка.
Если y = 2x и x = 3, то y будет всегда равна 6.
Если мы преобразуем это в функцию JavaScript, она будет выглядеть так:
function double(number) {
return 2 * number;
}В приведенном выше примере double является чистой функцией. Если передать ей 3, она вернет 6. Всегда.
React построен на этой концепции. React предполагает, что каждый компонент, который вы пишете, является чистой функцией. Это означает, что компоненты React, которые вы пишете, должны всегда возвращать один и тот же JSX при одинаковых входных данных:
function Recipe({ drinkers }) { return ( <ol> <li>Boil {drinkers} cups of water.</li> <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li> <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li> </ol> );}export default function App() { return ( <section> <h1>Spiced Chai Recipe</h1> <h2>For two</h2> <Recipe drinkers={2} /> <h2>For a gathering</h2> <Recipe drinkers={4} /> </section> );}Когда вы передаете drinkers={2} в Recipe, он вернет JSX, содержащий 2 чашки воды. Всегда.
Если вы передадите drinkers={4}, он вернет JSX, содержащий 4 чашки воды. Всегда.
Прямо как математическая формула.
Вы можете думать о своих компонентах как о рецептах: если вы следуете им и не добавляете новые ингредиенты в процессе приготовления, вы будете получать одно и то же блюдо каждый раз. Это «блюдо» — JSX, которое компонент предоставляет React для рендеринга.
Побочные эффекты: (не)преднамеренные последствия
Процесс рендеринга в React всегда должен быть чистым. Компоненты должны только возвращать свой JSX, а не изменять какие-либо объекты или переменные, существовавшие до рендеринга — это сделало бы их нечистыми!
Вот компонент, нарушающий это правило:
let guest = 0;function Cup() { // Bad: changing a preexisting variable! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> );}Этот компонент читает и записывает переменную guest, объявленную вне него. Это означает, что несколько вызовов этого компонента приведут к появлению разного JSX! Более того, если другие компоненты читают guest, они тоже будут генерировать разный JSX, в зависимости от того, когда они были рендерены! Это непредсказуемо.
Вернемся к нашей формуле y = 2x. Теперь, даже если x = 2, мы не можем быть уверены, что y = 4. Наши тесты могут провалиться, пользователи будут в недоумении, самолеты будут падать с неба — вы понимаете, как это может привести к запутанным ошибкам!
Вы можете исправить этот компонент, передав guest в качестве пропса вместо этого:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaSet() { return ( <> <Cup guest={1} /> <Cup guest={2} /> <Cup guest={3} /> </> );}Теперь ваш компонент является чистым, так как возвращаемый им JSX зависит только от проп guest.
В целом, не стоит ожидать, что ваши компоненты будут рендериться в каком-то конкретном порядке. Не имеет значения, вызовете ли вы y = 2x до или после y = 5x: обе формулы будут вычисляться независимо друг от друга. Точно так же каждый компонент должен только « думать самостоятельно», а не пытаться координироваться с другими или зависеть от них во время рендеринга. Рендеринг похож на школьный экзамен: каждый компонент должен вычислять JSX самостоятельно!
Обнаружение нечистых вычислений с помощью StrictMode
Хотя вы, возможно, еще не использовали их все, в React есть три вида входных данных, которые можно считывать во время рендеринга: props, state и context.. Вы всегда должны рассматривать эти входы как доступные только для чтения.
Если вы хотите изменить что-то в ответ на ввод пользователя, вам следует установить state вместо записи в переменную. Вы никогда не должны изменять уже существующие переменные или объекты во время рендеринга вашего компонента.
React предлагает «строгий режим», в котором он вызывает функцию каждого компонента дважды во время разработки. Вызывая функции компонентов дважды, строгий режим помогает найти компоненты, которые нарушают эти правила.
Обратите внимание, как в исходном примере отображались «Гость № 2», «Гость № 4» и «Гость № 6» вместо «Гость № 1», «Гость № 2» и «Гость № 3». Исходная функция была нечистой, поэтому её двукратный вызов привёл к сбою. Но исправленная чистая версия работает даже при каждом двукратном вызове функции. ** Чистые функции только вычисляют, поэтому их двукратный вызов ничего не изменит** — точно так же, как двукратный вызов double(2) не меняет возвращаемое значение, а двукратное решение уравнения y = 2x не меняет значение y. Одинаковые входы — одинаковые выходы. Всегда.
Строгий режим не влияет на рабочую среду, поэтому он не замедлит работу приложения для ваших пользователей. Чтобы включить строгий режим, вы можете обернуть ваш корневой компонент в <React.StrictMode>. Некоторые фреймворки делают это по умолчанию.
Локальная мутация: маленький секрет вашего компонента
В приведенном выше примере проблема заключалась в том, что компонент изменял уже существующую переменную во время рендеринга. Это часто называют «мутацией», чтобы звучало немного страшнее. Чистые функции не изменяют переменные за пределами области действия функции или объекты, которые были созданы до вызова — это делает их нечистыми!
Однако совершенно нормально изменять переменные и объекты, которые вы только что создали во время рендеринга. В этом примере вы создаете массив [], присваиваете его переменной cups, а затем с помощью push добавляете в него дюжину чашек:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>;}export default function TeaGathering() { const cups = []; for (let i = 1; i <= 12; i++) { cups.push(<Cup key={i} guest={i} />); } return cups;}Если бы переменная cups или массив [] были созданы вне функции TeaGathering, это стало бы огромной проблемой! Вы бы изменяли уже существующий объект, добавляя элементы в этот массив.
Однако это нормально, потому что вы создали их во время того же самого рендеринга, внутри TeaGathering. Никакой код за пределами TeaGathering никогда не узнает, что это произошло. Это называется «локальной мутацией» — это как маленький секрет вашего компонента.
Где вы можете вызвать побочные эффекты
Хотя функциональное программирование в значительной степени опирается на чистоту, в какой-то момент, где-то, что-то должно измениться. В этом и заключается смысл программирования! Эти изменения — обновление экрана, запуск анимации, изменение данных — называются побочными эффектами. Это вещи, которые происходят «на стороне», а не во время рендеринга.
В React побочные эффекты обычно относятся к обработчикам событий. Обработчики событий — это функции, которые React запускает, когда вы выполняете какое-либо действие — например, нажимаете кнопку. Хотя обработчики событий и определяются внутри вашего компонента, они не запускаются во время рендеринга! Поэтому обработчики событий не обязательно должны быть чистыми.
Если вы исчерпали все другие варианты и не можете найти подходящую обработку события для вашего побочного эффекта, вы все равно можете прикрепить его к возвращаемому JSX с помощью вызова useEffect в вашем компоненте. Это указывает React выполнить его позже, после рендеринга, когда побочные эффекты разрешены. Однако этот подход должен быть вашим последним средством.
По возможности старайтесь выражать свою логику только с помощью рендеринга. Вы удивитесь, как далеко это может вас завести!
Почему React заботится о чистоте?
Написание чистых функций требует некоторой привычки и дисциплины. Но это также открывает замечательные возможности:
- Ваши компоненты могут работать в другой среде — например, на сервере! Поскольку они возвращают одинаковый результат для одинаковых входных данных, один компонент может обслуживать множество запросов пользователей.
- Вы можете повысить производительность, пропуская рендеринг компонентов, входные данные которых не изменились. Это безопасно, поскольку чистые функции всегда возвращают одинаковые результаты, поэтому их можно безопасно кэшировать.
- Если некоторые данные изменяются в середине рендеринга глубокого дерева компонентов, React может перезапустить рендеринг, не тратя время на завершение устаревшего рендеринга. Чистота позволяет безопасно останавливать вычисления в любой момент.
Каждая новая функция React, которую мы создаем, использует преимущества чистоты. От извлечения данных до анимаций и производительности — поддержание чистоты компонентов раскрывает всю мощь парадигмы React.
Вывод
- Компонент должен быть чистым, что означает:
- Он занимается только своим делом. Он не должен изменять какие-либо объекты или переменные, существовавшие до рендеринга.
- Одинаковые входы — одинаковый выход. При одинаковых входах компонент всегда должен возвращать одинаковый JSX.
- Рендеринг может происходить в любой момент, поэтому компоненты не должны зависеть от последовательности рендеринга друг друга.
- Вы не должны изменять какие-либо входные данные, которые ваши компоненты используют для рендеринга. Сюда входят props, state и context. Чтобы обновить экран, «установите» state вместо изменения уже существующих объектов.
- Старайтесь выражать логику вашего компонента в возвращаемом JSX. Когда вам нужно «что-то изменить», обычно это лучше делать в обработчике событий. В крайнем случае, вы можете использовать
useEffect. - Написание чистых функций требует некоторой практики, но раскрывает всю мощь парадигмы React.
Рендеринг списков
Часто возникает необходимость отобразить несколько похожих компонентов из набора данных. Для работы с массивом данных можно использовать методы массивов JavaScript. На этой странице вы будете использовать filter() и map() с React для фильтрации и преобразования массива данных в массив компонентов.
Понимание интерфейса пользователя как дерева
Ваше приложение React приобретает очертания, и многие компоненты вложены друг в друга. Как React отслеживает структуру компонентов вашего приложения?