Соображения о том, как работать с эффектами в React. Советы и рекомендации, о которых вы, возможно, не слышали.

Это не учебник о том, как эффекты React работают внутри. Основная цель этой статьи — краткое описание лучших практик, советов и приемов. Если вам нужна подробная информация о том, как React Effects/Hooks работают внутри, вам могут пригодиться следующие ссылки:

Об React Effects в целом

Возможно, вы уже видели дискуссии о том, следует ли React принципам реактивного программирования или нет. Чтобы быть кратким здесь, позвольте мне ответить прямо сейчас. React не следует фундаментальным принципам реактивного программирования. Но в то же время можно сказать, что React «реактивный». Главное здесь в том, что реактивное программирование и реактивность — это не одно и то же. Подробнее можно узнать здесь.

Я начал этот раздел с реактивности, потому что важно понимать эффекты React. По сути, эффекты возможны благодаря реактивности.

Давайте рассмотрим простой фрагмент кода ниже:

Попробуем понять, что делает этот код построчно.

  • 1 — это определение фигуры штата. Мы хотим иметь значение counter, которое должно быть динамичным и наблюдаемым. Его можно изменить с помощью функции setCounter. Но как наблюдать его изменения?
  • 2 — Чтобы отслеживать изменяемые значения в React, мы можем использовать хук useEffect. То есть, когда значение counter изменяется, вызывается обратный вызов. Вот как React справляется с этим под капотом.
  • 3 — Вот как мы организуем изменения в наблюдаемой сущности. Опять же, нам нужен еще один useEffect, чтобы настроить его. Почему? Ну, это из-за того, как React рендерит. Как видите, список зависимостей здесь пуст, а это значит, что это действие происходит только один раз, и последующие рендеры больше его не вызывают. Когда вызывается обратный вызов интервала, counter обновляется, затем вызывается обратный вызов эффекта (2) и снова с начала.
  • 4 — Мы не хотим ничего рендерить. Затем возвращается null.

На самом деле, неправильно говорить, что counter является наблюдаемой величиной. Его вообще не видно! Мы можем увидеть его изменения благодаря внутреннему рендерингу React (см. ссылки выше). Тогда как описать термин React Effect простыми словами? Давай попробуем.

React Effect – это механизм планирования изменения значения, наблюдения за этим изменением и реагирования на него.

И еще раз о реактивности.

React Effects не следуют фундаментальным принципам реактивного программирования. Основное отличие заключается в деталях реализации. Но мы можем думать, что Эффекты зависят от особого типа Реактивности.

Используйте ESLint для проверки зависимостей

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

Существует множество стандартных правил ESLint, которые вы можете использовать из коробки. Но скорее всего их не хватит для быстрорастущего проекта. Затем на сцену выходят плагины ESLint. Эта тема достойна отдельной статьи для подробного описания. Сейчас нас интересует только плагин eslint-plugin-react-hooks. Это заставляет вас и вашу команду следовать Правилам крючков.

Есть способы отключить правило ESLint. И очень много ситуаций, когда разработчик думает, что он действительно знает, как должен работать его код, и не хочет следовать Правилам хуков. В таком случае разработчик, скорее всего, поступит следующим образом.

Просто помните следующее:

Если вы не хотите следовать Правилам хуков, это не значит, что вы лучше знаете свой код. Это означает, что вы не понимаете, как работают React Effects, и просто пытаетесь взломать систему!

Ниже мы рассмотрим уродливые хуки типа useComponentDidMount. В большинстве случаев они используют такие отключающие операторы. И это еще одна причина, по которой такие хуки — просто хаки.

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

Гочки

Система реактивности React основана на подходе, согласно которому разработчик знает, какие зависимости должны вызвать эффект. И есть некоторые ошибки, которые не могут быть проверены eslint-plugin-react-hooks.

1. Запомните функции в пользовательских хуках

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

Вы видите здесь проблему? Позвольте мне описать. Проблема в функции incrementCounter. component использует его в списке зависимостей для useEffect. Но эта функция не была запомнена в кастомном хуке useCounter. И это означает, что каждый рендер Component генерирует новую ссылку на функцию incrementCounter, и это приводит к тому, что Эффект повторяется!

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

Всегда удалять события за useEffect с. Если этого не делать, возникнут трудноустранимые проблемы.

Единственный законный вариант решения этой проблемы — запомнить функцию incrementCounter в хуке useCounter.

2. Будьте осторожны с функциями ввода

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

Здесь та же проблема, что и в предыдущем примере. Когда Component отображается, создается новая ссылка на встроенную функцию (*). Затем это вызывает неожиданные эффекты в пользовательском хуке useCounter.

Есть два способа это исправить. Первый способ — избавиться от этой встроенной функции и запомнить ее.

Это прекрасно работает. Но мне не нравится такой подход, он немного многословен.

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

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

Давайте рассмотрим этот код построчно.

  • 1 — мы сохраняем входную функцию как React Ref. Это легальный способ получить доступ к этой функции и не указывать ее в списке зависимостей.
  • 2 — Это важная часть этого подхода. Когда входная функция изменяется, мы должны обновить ссылку на входную функцию.
  • 3 — Затем, когда мы хотим использовать функцию ввода (onChange), мы вместо этого используем ссылку на нее (onChangeRef.current).
  • 4 — На данный момент, поскольку мы используем React Ref под капотом, мемоизация функции ввода является избыточной, и мы можем свободно встраивать ее.

Выглядит неплохо. Верно?

Вы можете спросить, как на самом деле работает этот ад. Есть простой принцип. Функции в JavaScript — это объекты. Затем, когда мы передаем встроенную функцию в качестве параметра, мы фактически передаем ссылку на функцию вместо самой функции. Когда ссылка обновляется, useEffect просто использует обновленную функцию ввода.

Если вы не знакомы с этим понятием, рекомендую посмотреть эту страницу. Если вы спешите, следующая диаграмма описывает это.

Соображения о методах жизненного цикла компонентов класса

Я думаю, у вас есть некоторый опыт работы с компонентами класса React. Существует набор Методов жизненного цикла. (Вы также можете найти эту диаграмму полезной.) Иногда разработчики любят смешивать функциональные компоненты и принципы жизненного цикла компонентов классов. Я имею в виду пользовательские хуки, такие как useComponentDidMount, useComponentWillUnmount и другие.

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

Эмм… Может быть, в некоторых крайних случаях такое смешивание может оказаться полезным. И это, безусловно, должно работать нормально, если в приложении есть правило, рассматривающее это как приемлемый подход к созданию эффектов. Но я по-прежнему считаю такой код уродливым и нарушающим принципы реактивности React! И даже приводит к трудновоспроизводимым проблемам, если вы не являетесь опытным хакером React.

Попробуем понять, что не так.

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

Но вы можете сказать здесь, что есть законный обходной путь с React Refs, и мы можем создавать эти пользовательские хуки с его помощью! Ты прав. Но это не сильно меняет положение вещей. Реактивность остается нарушенной независимо от того, используете ли вы легальное или нелегальное решение.

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

Просто попробуйте построить эту картинку в своей голове. React предоставляет вам современный API, гораздо более гибкий и мощный, чем старые компоненты на основе классов. И вы до сих пор не можете перестроить свой разум и продолжать пользоваться устаревшими понятиями. Вы видите, что я имею в виду?

Перестаньте придерживаться старых концепций React, построенных вокруг компонентов на основе классов. Освежите свой разум и создавайте приложения React на основе эффектов! И без навязчивых попыток взломать систему и внедрить кастомное уродливое API типа useComponentDidMount.

Почему мы должны деструктурировать функции

Есть еще одна тема, которую я хотел бы обсудить с вами. Это связано с Правилами хуков. Если вы уже использовали eslint-plugin-react-hooks, то наверняка сталкивались с ситуацией, когда Правила хуков не позволяют использовать функцию прямо из «реквизита» (типа props.onChange). Вы всегда вынуждены деструктурировать его перед добавлением в список зависимостей. В большинстве случаев разработчик просто отключает это предупреждение с помощью оператора отключения ESLint. И это неправильно.

Этот код нарушает правила хуков:

Этот код соответствует Правилам хуков:

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

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

Проблема в чистоте. React хочет видеть чистую функцию в списке зависимостей. Вызов props.onChange() нельзя считать чистым, поскольку объект props является this для onChange.

Но почему это важно? Это потому, что изменения должны быть наблюдаемыми. Помните нашу дискуссию о реактивности выше?

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

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Посетите наш Community Discord и присоединитесь к нашему Коллективу талантов.