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

По названию можно предположить, что Реакторы как-то связаны с React. Но на самом деле в некотором смысле верно обратное. Я использовал реакторы, чтобы максимально исключить представление (например, React) от логики моего приложения. Название Реакторы возникло при попытке объединить Redux с актёрами, упомянутыми в этой
статье
(не путать с этими актёрами).

Проблема

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

Важно понимать, что Reactors основаны на знании предыдущего состояния, а также текущего состояния. Если бы я обращал внимание только на текущее состояние, я бы не смог определить, вошел ли я только что по заданному маршруту и ​​нужно ли загрузить данные, необходимые для этого маршрута или я уже был в этот маршрут и уже сделал запрос на загрузку этих данных (и я просто жду какого-то асинхронного ответа). Таким образом, понятие предыдущего состояния существенно в этом контексте.

Влияния

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

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

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

Состояние, вычисляемые свойства и реактивность

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

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

Это тонкое различие, и я не на 100% уверен, что здесь мы работаем с правильным набором абстракций. Оказывается, многие идеи Redux и React уже давно существуют в теории систем управления. В React у нас явно есть свойства (u в теории систем управления) и состояния (x в теории систем управления). В React они рассматриваются как объекты первого класса. Это косвенно обсуждается во введении к React Thinking in React. В Redux у нас есть аналогичное отображение с действиями u и состояниями x. Но на самом деле у нас нет первоклассного представления о выходе (y в системах управления).

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

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

Выполнение

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

Концептуально представьте, что у нас есть функция-редуктор с именем «f».
Мы могли бы придумать другую функцию-редуктор, «f2», следующим образом:

function f2(state, action) {
  let initialState = state;
  let finalState = f(state, action);
  return reactor(initialState, finalState);
}

Здесь вы можете видеть, что Reactor просто принимает предыдущее состояние, текущее состояние, а затем возвращает (потенциально новое) состояние. Но функция `f2` по-прежнему является редуктором. Все это является внутренним и не имеет побочных эффектов (то есть, если вы вызовете функцию с теми же аргументами, вы получите тот же результат).

Это устраняет две проблемы, которые у меня были с моделью «актер», о которой я упоминал ранее. Во-первых, следует избегать использования «подписаться», потому что на самом деле нет никаких гарантий относительно того, когда подписчики будут уведомлены или в каком порядке. Это позволяет избежать потенциальных проблем с отправкой действий обратно в хранилище и бесконечных циклов. Ни один из них не в игре здесь. Реактор вызывается внутри редуктора еще до того, как магазин увидит результат. Кроме того, у Reactor есть только один шанс срабатывания (и если это ограничение не работает для вас… я думаю, вам следует пересмотреть логику вашего приложения).

Оказывается, бывают случаи, когда мы хотим иметь несколько реакторов, обертывающих один и тот же редьюсер. Это решается путем их рекурсивной упаковки. Фактическая команда (используя вада) будет:

const reducer = wrapReducer(baseReducer, [reactor1, reactor2]);

На практике это будет эквивалентно:

function reducer(state, action) {
  let s0 = state;
  let s1 = baseReducer(s0, action);
  let s2 = reactor1(s0, s1);
  let s3 = reactor2(s0, s2);
  return s3;
}

Обратите внимание, что «реактор2» видит разницу между начальным состоянием и
то, что возвращает «реактор1». Другими словами, любые изменения, внесенные «реактором1» в состояние, включаются в изменения, видимые «реактором2». Это делает весь процесс модульным (можно составить несколько реакторов), синхронным (точно так же, как редуктор), функциональным (без побочных эффектов) и детерминированным.

Когда использовать реакторы

Как я уже говорил, между вычисляемым свойством и Reactor есть тонкая разница. Селекторы в «reselect» принимают текущее состояние и возвращают некоторое значение. Чтобы не путать реакторы с селекторами, я намеренно сделал предыдущее состояние первым аргументом реактора. Таким образом, любой реактор вынужден объявлять предыдущее состояние в своем списке аргументов. Если вы его не используете… вам не нужен Reactor. Если бы я поставил конечное состояние на первое место, я представил, что многие люди используют его просто как способ вычислить что-то на основе текущего состояния. Для этого их лучше обслуживать с помощью чего-то вроде `reselect`.

Стоит отметить, что библиотека vada также включает некоторые функции, которые помогут вам избежать использования Reactors. Отличным местом для внедрения вычисляемых свойств является распространение свойств по дереву компонентов в приложении React. Библиотека react-redux предоставляет функцию connect. Я создал аналогичную функцию под названием bindClass. Основное отличие состоит в том, что я хотел что-то, что обеспечивало ограничения типов, чтобы не забыть проп по пути. Я не мог придумать определения типа для подключения, которые бы меня удовлетворили. Но концептуально они очень похожи. По тем же причинам я также включил некоторые функции запоминания, которые, опять же, предоставляют ограничения типа, чтобы избежать потери информации о типе. Поскольку обе эти возможности относительно просты, я не чувствовал (слишком) вины за их повторную реализацию.

Вывод

Реакторы — это полезный шаблон для инкапсуляции логики приложения. Различие
между Reactor и обычным вычисляемым свойством заключается
в том, что Reactor зависит как от текущего, так и от предыдущего состояния
и предоставляет (потенциально) новое значение состояния для полного состояние.

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

Хотя я публикую библиотеку и сопутствующую документацию по этим шаблонам, я не чувствую, что пыль еще не осела. Как я упоминал ранее, я чувствую, что обработка изменений состояния и вычисляемых свойств в настоящее время рассматривается как второсортная в подходе Redux. Так что все это попытка перейти к первоклассным абстракциям для этих вещей.