Предупреждение: я средний программист на React. Поэтому я не уверен, что мои примеры идеальны или не содержат ошибок.
В моем последнем посте я объяснил основные функции и концепции GState. В этой статье я буду следовать своим мнениям с помощью шаблонов для решения некоторых проблем управления состоянием:
- Однонаправленный поток данных
- Государственные декларативные компоненты (в духе Relay / Apollo).
- Инкапсуляция в едином хранилище состояний (Redux issue).
Однонаправленный поток данных
GState может применять однонаправленный поток данных, такой как Flux или Redux. Возьмем классический пример Todo от Redux и перепишем его с помощью GState (Исходный код):
Команды: Запишите сторону в шаблоне CQRS.
Команды такие же, как и в редюсерах Redux. Это могут быть чистые функции, принимающие состояние в качестве параметра и использующие state.get для чтения, state.set для изменения. Это важно, потому что вы можете повторно использовать бизнес-логику команды для любого контекста / пути состояния. Также легко проверить:
const state = new GState(); state.set({ todos: {} }); addToDo(state, "a"); expect(state.get({ todos: { _: 1 }})) .toMatchObject({ todos:[{text: "a", completed: false}]});
Запросы: чтение сайта в шаблоне CQRS.
В запросах используется компонент высокого порядка декларативного состояния gstate (то же самое, что и в Appollo graphql или Relay createFragmentContainer), которые используют state.watch для запроса информации и визуализации компонентов без состояния.
Государственные декларативные компоненты
Еще важнее то, что эти компоненты являются декларативными: они позволяют разработчикам указывать как должен выглядеть пользовательский интерфейс для данного состояния, и им не нужно беспокоиться о том, как , чтобы показать этот пользовательский интерфейс - Ретрансляционное мышление
В нашем идеальном мире компонент должен заботиться только о рендеринге состояния , не зная, как, когда и откуда.
// Expects the props to have the following shape: // { // text, // isComplete // } const TodoItem = ({ text, isComplete }) => { return ( <View> <Checkbox checked={isComplete} /> <Text>{text}</Text> </View> ); } } // gstate is an HOC watch/get declarative state for the component. const TodoItemWithState = gstate({ text: 1, isComplete: 1 }, (props, data) => { return <TodoItem text={data.text} isCompleted={data.isCompleted} /> })
Gstate HOC
Это моя тривиальная версия gstate HOC. Это должно быть основной функцией декларативного шаблона состояния.
Компонентный состав
Сложные декларативные компоненты с большим состоянием могут быть созданы путем компоновки более мелких компонентов. Есть 2 подхода:
- Родительский компонент может объявлять форму данных как для детей, так и для себя.
- Родитель использует state.path (key) для создания подсостояния для детей, которые декларируют свои собственные требования к состоянию.
// Expects a `list` with a string `title`, as well as the information // for the `<TodoItem>`s (we'll get that next). const TodoList = ({ state, title, todos }) { render() { return ( <View> <Text>{title}</Text> {todos.map(key => <TodoItem state={state.path(["todos", key])} />)} </View> ); } } const TodoListWithState = gstate({ title: 1, todos: { _: "$key" // todos.map((item, key) => key) } }, (props, data) => ( <TodoList state={props.state} title={data.title} todos={data.todos}/> ))
Примечание: это тот же образец, что и для композита вида / модели вяз.
view : Model -> Html Msg view model = div [ class "todomvc-wrapper" , style [ ( "visibility", "hidden" ) ] ] [ section [ class "todoapp" ] [ lazy viewInput model.field // child component with sub state , lazy2 viewEntries model.visibility model.entries // child component with sub state , lazy2 viewControls model.visibility model.entries // child component with sub state ] , infoFooter ]
Инкапсуляция в едином государственном хранилище
Есть пара проблем с инкапсуляцией Redux, которые я пытаюсь решить с помощью GState. В целом GState решает проблему инкапсуляции с помощью:
- Изолированное подсостояние с помощью API state.path.
- Команды чистой функции с состоянием в качестве параметра.
Проблема №1: два состояния, один редуктор
Команды GState - это чистые функции, принимающие состояние в качестве параметра, поэтому нет проблем с повторным использованием с любым деревом состояний / контекстом.
function incrementCounter(state){ const counter = state.get("counter") || 0; state.set({ counter: counter+1}) }
Создайте два компонента Counter с разными путями состояния
<Counter state={state.path("top")}/> <Counter state={state.path("bottom")}/>
Компонент счетчика применяет incrementCounter к своему собственному состоянию
const Counter = ({state}) => ( <button onClick={() => incrementCounter(state)}>+</button> )
Проблема # 2: прислушиваемся к другим редукторам
Вы можете перейти в родительское состояние, чтобы наблюдать за обоими состояниями счетчиков.
<Counter state={state.path("top")}/> <Counter state={state.path("bottom")}/> state.watch({ top:{ counter: 1 }, bottom: { counter: 1} }, res => { total = res.top.counter + res.bottom.counter })
Проблема # 5: экземпляры динамического состояния
Создать новое дерево состояний для динамического компонента очень просто.
const AddDynamicCounters = ({ state, numberOfCounters}) => { const counterArr = []; for (let i = 0; i < numberOfCounters; i++) { counterArr.push(i); } return ( <div>{counterArr.map(id => <Counter state={state.path(i)}/>)}</div> ) }
Инкапсулируйте команды с компонентами.
Первый раз пробуя Redux, я задаюсь вопросом, как повторно использовать мои компоненты и редукторы. Вскоре я понимаю, что Redux не пытается этого сделать.
Дело в том, что Flux (Redux) перемещает состояние приложения из состояния компонента и бизнес-логику из обработки событий. Здорово, что можно писать состояние приложения + бизнес-логику без каких-либо компонентов. И вы можете повторно использовать свой код для любых UI-фреймворков.
Но в некоторых случаях допустимо инкапсулировать как представление, так и состояние. Например, разделите сложное приложение на модули.
Вы можете просто добавить команду createCounter в модуль counter.js. Затем вы можете повторно использовать модуль для создания компонента Counter.
// add a create component command function createCounter(state){ //init state //return component with query return gstate({ counter: 1 },() => ( <button onClick={() => incrementCounter(state)}>+</button> )) } function incrementCounter(state){ const counter = state.get("counter") || 0; state.set({ counter: counter+1}) }
Финал
В следующий раз я сначала изучу шаблоны выборки данных, связи между компонентами, путешествий во времени, SSR и автономного режима для PWA.