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

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, всем компонентам, которые могут влиять на общее состояние. Для ясности я собираюсь указать только на части кода, которые отличаются от учебника Джареда.

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

open TodoApp;
let make = _children => {
...component,
initialState: () => { ... },
reducer,
render: ({state, send}) => {
<TodoForm appSend=send />
<TodoList appState=state appSend=send />
}
};
view raw App.re hosted with ❤ by GitHub

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

/* TodoList.re */
let make = (~appState: TodoApp.state, ~appSend, _children) => {
...component,
render: _self =>
<div>
/* Map appState.items, and pass item into... */
<TodoItem item appSend key=(string_of_int(item.id)) />,
/* ... */
</div>
};
/* TodoItem.re */
let make = (~item: TodoApp.item, ~appSend, _children) => {
...component,
render: _self =>
<div onClick=(_event => appSend(TodoApp.ToggleItem(item.id)))>
/* Checkbox input with title of item */
</div>
};

Когда компонент 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, который намного проще понять и использовать.

/* Before: */
onClick={self.reduce(_event => Click)}
didMount: self => {self.reduce(() => Click, ()); NoUpdate}
/* Wut? What's going on here? */
/* After: */
onClick={_event => self.send(Click)}
didMount: self => {self.send(Click); NoUpdate}
/* OK... Much better. */
view raw ReduceVsSend.re hosted with ❤ by GitHub

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

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

Кредиты

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

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