Важное обновление: я опубликовал обновленную статью по этой теме, в которой устранена серьезная проблема с реализацией, описанной в этой статье.

TL; DR: Я изменил руководство Джареда Форсайта по ReasonReact, улучшив управление состоянием приложения. Отправляя два свойства appState и appSend всем дочерним компонентам, мы позволяем им напрямую влиять на общее состояние, вместо того, чтобы полагаться на сложную сеть обратных вызовов. Звучит интересно?

Go on…

Хотите реализовать общее состояние в приложениях ReasonReact ..?

Наверное, нет, если я буду практичным. Вероятность того, что вы, читатель, даже слышали о ReasonML, не говоря уже о ReasonReact, невелика. Хотя Reason упоминался в theStateOfJs 2017, ~ 80% респондентов заявили, что никогда о нем не слышали. Так что, если нет, ничего страшного. Перейдите на домашнюю страницу ReasonML прямо сейчас и попробуйте этот новый (вроде?) Язык. Это круто, он создан разработчиками React и опирается на мощь OCaml.

Однако, если вы относитесь к ~ 19,2%, которые слышали о ReasonML, или ~ 0,8%, которые действительно пробовали его и интересовались, как может работать общее / глобальное состояние, это учебник для вас! Весь код, о котором я буду говорить в этой статье, доступен в этом репо.

Но сначала немного предыстории

В прошлом году Джаред Форсайт опубликовал Учебное пособие по ReasonReact - отличную отправную точку для людей, заинтересованных в создании приложений React с помощью ReasonML. Учебник включает в себя создание простого приложения списка Todo с использованием редукторов для управления состоянием приложения - функция, изначально предоставляемая ReasonReact. Структура приложения выглядит примерно следующим образом:

App implements reducer(action)
  -> TodoItem onToggle=send(Toggle)
  -> TodoInput onSubmit=send(AddItem(text))

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

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

App implementes editCallback(), deleteCallback(), ...
  -> TodoItem editCallback=editCallback deleteCallback=deleteCallback...
    -> TodoInlineEditor editCallback=editCallback deleteCallback=deleteCallback...
      => Use editCallback()
      -> TodoDeleteButton deleteCallback=deleteCallback
        => Use deleteCallback()

Блэр Андерсон написал отличную статью под названием Вероятно, вам не нужен Redux, в которой объясняется, как в приложениях React малого и среднего размера часто достаточно передать два свойства - appState и setAppState всем компонентам, что позволяет использовать вложенные компоненты. прямой доступ к общему состоянию, минуя необходимость добавления и ретрансляции отдельных обратных вызовов.

Моя реализация учебника Джареда смешивает эту идею и отправляет два реквизита, appState и appSend, всем компонентам, которые могут влиять на общее состояние. Для ясности я собираюсь указать только на части кода, которые отличаются от учебника Джареда.

Разумная альтернатива

appState, как видно из названия, является общим состоянием, сохраняемым корневым компонентом. appSend - это send метод, доступный для render метода корневого компонента. Мы передаем его вместе с appState, чтобы дочерние компоненты могли запускать редуктор корневого компонента. Компоненту TodoForm не требуется свойство appState, потому что он никогда не считывает общее состояние.

Когда компонент TodoItem использует опору appSend, он передает действие TodoApp.ToggleItem. Чтобы правильно разрешить его как type action, нам нужно явно указать пространство имен - общий модуль TodoApp. Поскольку всем компонентам необходим доступ к определению типа состояния и всем возможным действиям, они должны быть помещены в этот отдельный общий модуль.

Моя первая попытка реализовать этот шаблон сохранила типы state и action в модуле корневого компонента. Однако это просто привело к проблеме циклической зависимости: App -> child -> App, блокирование компиляции. Это исправлено при извлечении общего кода в другой модуль.

Вот и все. Обратите внимание, что нигде нет функций обратного вызова?

Преимущества и несколько предостережений

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

И учитывая, что мы используем язык с сильной, предполагаемой и, что самое главное, надежной системой типов, мы также получаем все ее преимущества.

Одна вещь, которая меня полностью не устраивает, - это то, что меня вынудили open TodoApp (первая строка в App.re сущности выше) в модуле App. TodoApp определяет типы state и action, на которые опирается App компонент, и я постоянно сталкивался с синтаксическими ошибками, когда пытался аннотировать типы. Все остальные модули имеют ручную аннотацию типа, когда они сначала ссылаются либо на общий state, либо на action. Открытие TodoApp в модуле App - не проблема, поскольку это корневой компонент.

Возвращаясь к ранее надуманному примеру с нашим новым шаблоном, мы получаем:

TodoApp implements state and action types, and the reducer.

App uses TodoApp.state, TodoApp.action and TodoApp.reducer
  -> TodoItem appState=App.state appSend=App.send
    -> TodoInlineEditor appState=App.state appSend=App.send
      => props.appSend(TodoApp.Edit(todoId, text))
      -> TodoDeleteButton appSend=App.send
        => props.appSend(TodoApp.Delete(todoId))

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

Заключение

Я работал над личным проектом Turaku - менеджером паролей для команд с ES6 + React последние пару месяцев, и его возрастающая сложность означала, что боль от рефакторинга и добавления новых функций постепенно уменьшалась с от раздражения до жаля. В своих поисках более надежной среды я склонялся к Typescript, когда Jasim и Sherin из Protoship.io предложили ReasonML в качестве (лучшей) альтернативы.

Пока я все еще привыкаю к ​​ReasonML и ReasonReact, мои первые впечатления в целом положительные. И язык, и библиотека в настоящее время стремительно развиваются. Например, пока я работал с руководством Джареда и пытался адаптировать его к своим предпочтениям, ReasonReact обновил и заменил метод self.reduce на метод self.send, который намного проще понять и использовать.

Однако сообщения об ошибках компилятора ReasonML иногда могут сбивать с толку. И слишком часто он просто лает, что есть что-то не так с моим синтаксисом в строке, сообщает мне, что из-за этого произошел сбой, и просит меня зарегистрировать ошибку в репозитории Reason. ¯\_(ツ)_/¯ Боли роста, наверное.

В течение следующих двух месяцев я попытаюсь полностью переписать код Тураку в ReasonML. Я использовал create-react-app, поэтому наличие сценариев причин упрощает начало работы. Я уверен, что напишу больше об этом позже. :-)

Кредиты

Искусство Рекхи Соман: www.rekhasoman.com

Первоначально опубликовано на turaku.com.