Важное обновление: я опубликовал обновленную статью по этой теме, в которой устранена серьезная проблема с реализацией, описанной в этой статье.
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 /> | |
} | |
}; |
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. */ |
Однако сообщения об ошибках компилятора ReasonML иногда могут сбивать с толку. И слишком часто он просто лает, что есть что-то не так с моим синтаксисом в строке, сообщает мне, что из-за этого произошел сбой, и просит меня зарегистрировать ошибку в репозитории Reason. ¯\_(ツ)_/¯
Боли роста, наверное.
В течение следующих двух месяцев я попытаюсь полностью переписать код Тураку в ReasonML. Я использовал create-react-app
, поэтому наличие сценариев причин упрощает начало работы. Я уверен, что напишу больше об этом позже. :-)
Кредиты
Искусство Рекхи Соман: www.rekhasoman.com
Первоначально опубликовано на turaku.com.