Когда компоненты React перешли от классов к функциям, весь шум был о переходе от объектно-ориентированного программирования к функциональному программированию. Функциональное программирование преследует две цели:

  1. Вы получите те же результаты, если предоставите одинаковые входные данные для функции.
  2. Единственная цель функции - создать результат. Он не пытается делать что-то на стороне. (Функции, которые влияют на что-то другое, кроме их собственного вывода, описываются как функции, которые создают побочные эффекты.)

Если функция может удовлетворять этим двум критериям, она называется чистой функцией. Вы можете запускать одни и те же входные данные в течение всего дня и всегда получать один и тот же результат, без необходимости отслеживать что-либо еще.

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

Затем появились Крючки.

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

Итак, что остается, если функциональные компоненты больше не следуют парадигме объектно-ориентированного или функционального программирования?

Разрешите представить вам идею конечных автоматов.

Конечные автоматы: старое состояние + ввод = новое состояние

Концепция конечных автоматов была представлена ​​миру еще в 1943 году в статье по нейробиологии Уоррена Маккаллоха и Уолтера Питтса. Они пытались смоделировать поведение, основываясь на идее, что, если у вас есть определенный набор входных данных, вы можете определить конкретный набор возможных состояний на основе текущего состояния системы. Когда состояние изменяется из-за изменения входных данных, конечный автомат подвергается так называемому переходу. Когда конечный автомат получает сообщение перехода, он переходит к одному из ограниченного числа других состояния, в зависимости от того, что это было за сообщение.

Это довольно много, чтобы обернуть голову одним абзацем, поэтому давайте рассмотрим классический пример из реальной жизни: светофор.

У светофора есть три определенных состояния: зеленый (движение), желтый (осторожно) и красный (остановка). Когда индикатор загорается зеленым, таймер ведет обратный отсчет. Когда таймер истекает, он сообщает свету перейти в следующее состояние: желтый. Он сбрасывается, начинает обратный отсчет, а затем сообщает свету перейти в следующее состояние: красный. В этот момент он также сообщает светофору на другой дороге перейти к следующему состоянию, с красного на зеленый, и запускает тот же цикл для светофора на другой улице.

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

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

Крючки как государственные машины

Итак, как знание конечных автоматов помогает нам лучше понять хуки?

Если мы посмотрим на хуки, мы увидим, что они делятся на три категории:

  • Хуки, запускающие повторную визуализацию при обновлении (useState, useContext)
  • Хуки, обновляющие кеш при изменении значений (useMemo, useCallback)
  • Хуки, срабатывающие при изменении значений (useEffect, useLayoutEffect)

(Да, useRef не попадает в эти категории, но это странная птица.)

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

Пример из реальной жизни: модальные окна подтверждения

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

Если мы посмотрим на наш собственный компонент, мы можем взглянуть на него с точки зрения различных частей состояния, которые нам нужно удерживать:

  • Пользователь хотел заменить весь заказ?
  • Отправил ли пользователь запрос на изменение заказа?
  • Видно ли модальное окно подтверждения?
  • Подтвердили ли замену, если нужно?
  • Обновление завершено?

Глядя на эти различные части состояния, мы можем видеть, какие части должны вызывать какие изменения, и определять различные состояния, в которых может находиться наш компонент:

Глядя на то, как наши переменные определяют конечный автомат, мы получаем три преимущества:

  1. Мы упрощаем наши 32 различных возможных комбинации переменных (истина или ложь, 5 различных способов) до 7 различных состояний компонентов. Вместо того, чтобы думать в терминах «что правда» и «что ложь», мы можем думать в терминах «что компонент настроен делать прямо сейчас?»
  2. Из этих 7 состояний мы можем четко определить, как одно состояние превращается в другое. От «Не заменять» мы можем перейти только к «Заменяет» или «Обновление». От «Заменить» мы можем только перейти к «Не заменять» или «Запрошено изменение». Если каким-то образом мы получим какую-то странную комбинацию переменных, мы знаем, что что-то пошло не так, и отлаживаем оттуда.
  3. Возможно, наиболее важно то, что конечные автоматы позволяют нам управлять потоком пользователей, проверяя данные компонента, а не отслеживая поток функций. Обработчики ориентированы на изменение данных, а не на управление потоком. Это позволяет нам сосредоточиться на модели React, ориентированной на данные, вместо того, чтобы пытаться вернуться к императивной, основанной на потоке модели. (Обратите внимание, как мы используем showConfirmation как голое значение для отображения нашего модального окна, вместо того, чтобы оборачивать его сеттер _10 _ / _ 11_ функциями, что может быть заманчивым способом сохранить императивное настроение.)

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

Итак, у нас есть наши состояния и переменные, которые ими управляют. Что теперь?

Введите useEffect.

useEffect в качестве менеджера переходов конечного автомата

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

Если мы преобразуем приведенную выше таблицу событий в обработчики событий и useEffect хуки, мы получим что-то вроде этого:

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

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

Связав изменения с состоянием, становится яснее, как текущие данные влияют на поведение пользователя, вместо того, чтобы пытаться определить поведение, имитируя поток императивного программирования. Это помогает нам оставаться в настроении «следить за данными», которое поощряет React.

Как только мы увидим, как переходят отдельные состояния, мы можем объединить часть кода, чтобы сделать его более компактным:

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

Полный, полнофункциональный пример можно найти по адресу https://r4dll.csb.app/.