Сохранение чистоты компонентов
Некоторые функции JavaScript являются чистыми. Чистые функции выполняют только вычисления и ничего больше. Строго следуя принципу чистоты при написании компонентов, вы можете избежать целого класса запутанных ошибок и непредсказуемого поведения по мере роста вашего кода. Однако для получения этих преимуществ необходимо соблюдать несколько правил.
Вы узнаете
- Что такое чистота и как она помогает избежать ошибок
- Как сохранять чистоту компонентов, предотвращая изменения в фазе рендеринга
- Как использовать Строгий режим для поиска ошибок в ваших компонентах
Чистота: Компоненты как формулы
В информатике (и особенно в мире функционального программирования), чистая функция обладает следующими характеристиками:
- Она занимается только своим делом. Она не изменяет какие-либо объекты или переменные, существовавшие до её вызова.
- Одинаковые входные данные, одинаковый результат. При одинаковых входных данных чистая функция всегда должна возвращать одинаковый результат.
Возможно, вы уже знакомы с одним примером чистых функций: формулы в математике.
Рассмотрим следующую математическую формулу: 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 cups of water. Всегда.
Если вы передадите drinkers={4}, она вернёт JSX, содержащий 4 cups of water. Всегда.
Точно так же, как математическая формула.
Вы можете рассматривать ваши компоненты как рецепты: если вы следуете им и не вводите новые ингредиенты во время процесса приготовления, вы каждый раз получите одно и то же блюдо. Это «блюдо» — 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 самостоятельно!
Подробное изучение
Обнаружение нечистых вычислений с помощью Строгого режима
Хотя вы, возможно, еще не использовали все, в React есть три типа входных данных, которые вы можете прочитать во время рендеринга: свойства, состояние и контекст. Вы всегда должны рассматривать эти входные данные как неизменяемые.
Когда вы хотите изменить что-либо в ответ на ввод пользователя, вы должны изменить состояние, а не писать в переменную. Вы никогда не должны изменять существующие переменные или объекты во время рендеринга компонента.
React предлагает «Строгий режим», в котором он вызывает функцию каждого компонента дважды во время разработки. Вызывая функции компонентов дважды, Строгий режим помогает найти компоненты, нарушающие эти правила.
Обратите внимание, как в исходном примере отображались «Гость №2», «Гость №4» и «Гость №6» вместо «Гость №1», «Гость №2» и «Гость №3». Исходная функция была нечистой, поэтому вызов её дважды привёл к нарушению. Но исправленная чистая версия работает даже если функция вызывается дважды каждый раз. Чистые функции только вычисляют, поэтому вызов их дважды ничего не изменит — так же, как вызов double(2) дважды не меняет возвращаемое значение, и решение y = 2x дважды не изменяет значение y. Одинаковые входные данные, одинаковые выходные данные. Всегда.
Строгий режим не оказывает никакого влияния на производительность в режиме работы, поэтому он не замедлит приложение для ваших пользователей. Чтобы включить Строгий режим, вы можете обернуть свой корневой компонент в <React.StrictMode>. Некоторые фреймворки делают это по умолчанию.
Локальное изменение: маленькая тайна вашего компонента
В приведенном выше примере проблема заключалась в том, что компонент изменял существующую переменную во время рендеринга. Это часто называют «изменением», чтобы сделать его звучание немного страшнее. Чистые функции не изменяют переменные вне области действия функции или объекты, созданные до вызова — это делает их нечистыми!
Однако полностью допустимо изменять переменные и объекты, которые вы только что создали во время рендеринга. В этом примере вы создаете массив [], присваиваете его переменной cups, а затем добавляете в него дюжину чашек:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaGathering() { let 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.
- Рендеринг может происходить в любое время, поэтому компоненты не должны зависеть от последовательности рендеринга друг друга.
- Вы не должны изменять какие-либо входные данные, которые ваши компоненты используют для рендеринга. Это включает свойства, состояние и контекст. Чтобы обновить экран, «установите» состояние вместо изменения существующих объектов.
- Старайтесь выражать логику вашего компонента в возвращаемом JSX. Когда вам нужно «изменить вещи», вы обычно захотите сделать это в обработчике событий. В крайнем случае, вы можете
useEffect. - Написание чистых функций требует немного практики, но оно открывает возможности парадигмы React.
Попробуйте выполнить несколько заданий
Задание 1 из 3:
Исправить сломанные часы
Этот компонент пытается установить CSS-класс <h1> для "night" в период с полуночи до шести часов утра, и "day" в другое время. Однако он не работает. Можете ли вы исправить этот компонент?
Вы можете проверить, работает ли ваше решение, временно изменив часовой пояс компьютера. Когда текущее время находится между полуночью и шестью часами утра, часы должны иметь инвертированные цвета!
export default function Clock({ time }) { let hours = time.getHours(); if (hours >= 0 && hours <= 6) { document.getElementById('time').className = 'night'; } else { document.getElementById('time').className = 'day'; } return ( <h1 id="time"> {time.toLocaleTimeString()} </h1> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/keeping-components-pure