Предупреждение: я средний программист на React. Поэтому я не уверен, что мои примеры идеальны или не содержат ошибок.

В моем последнем посте я объяснил основные функции и концепции GState. В этой статье я буду следовать своим мнениям с помощью шаблонов для решения некоторых проблем управления состоянием:

  1. Однонаправленный поток данных
  2. Государственные декларативные компоненты (в духе Relay / Apollo).
  3. Инкапсуляция в едином хранилище состояний (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 решает проблему инкапсуляции с помощью:

  1. Изолированное подсостояние с помощью API state.path.
  2. Команды чистой функции с состоянием в качестве параметра.

Проблема №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.