Простой state manager для простой работы

image




Аннотация


В фронтэенде многие предпочитают (или хотели бы) использовать лёгкие и простые пакеты. Кроме того, на текущий момент использовать средства управления состоянием — это стандарт. Я постарался объединить эти принципы и сделать новый state mangerstatirjs. Идеологической основой послужили: rematch, redux.


Цель статьи


Дать краткий обзор основному функционалу statirjs. Без сравнений и лишней теории.


Область применения


Основной областью применения statirjs являются малые/личные проекты, которым не требуются: многочисленные внешние зависимости, повышенный теоретический порог для использования средства управления состоянием.


Причины создания


  1. желание иметь простой redux как rematch;
  2. перенасыщенность выделенными сущностями в reduxстатья;
  3. стремление к отсутствию внешних зависимостей;
  4. необходимость независимости платформы для её развития;
  5. стремление к малому размеру;

Основные плюсы statirjs


1. он мало весит


  • ядро ~2.2 KB
  • коннектор к react ~0.7 KB

2. использует компонентный подход


  • весь store разбит на небольшие фрагменты — forme (читай "форма")
  • в каждой forme описывается и состояние и функции изменения этого состояния

3. удобно и легко расширяется


  • middlewares почти как у redux, только проще
  • upgrades почти как middlewares, только изменяют сам store

4. почти не требует писать бойлерплейтов


5. redux-devtool из коробки


6. работает с react через хуки


Примечание: к относительным плюсам можно отнести переиспользование популярного словоря терминов из redux. также statirjs написан на typescript и неплохо выводит типы как для forme так и для store.

На практике


Предлагаю оценить statirjs на практике. Ниже представлено весь необходимый код для инкрементации состояния:


import { createForme, initStore } from "@statirjs/core";

const counter = createForme(
  {
    count: 0,
  },
  () => ({
    actions: {
      increment: (state) => ({ count: state.count + 1 }),
    },
  })
);

const store = initStore({
  formes: {
    counter,
  },
});

Что здесь происходит?


  1. в фабрику createForme передаётся начальное состояние и функция;
  2. второй аргумент createForme (функция) возвращает объект с actions;
  3. в actions определена функция increment;
  4. increment получает состояние forme counter до вызова и после выполнения возвращает новое, следующее состояние;
  5. созданный counter передаётся в initStore для создания стора;

Для удобства можно вынести и переиспользовать все состовляющие forme:


const initState = {
  count: 0,
};

const actions = {
  increment: (state) => ({ count: state.count + 1 }),
};

const builder = () => ({ actions });

const counter = createForme(initState, builder);

const store = initStore({
  formes: {
    counter,
  },
});



Запоминаем №1: statirjs описывает действия как простые, чистые функции




Представим что нужно декрементировать значение. С statirjs это будет быстро и просто:


const counter = createForme(
  {
    count: 0,
  },
  () => ({
    actions: {
      increment: (state) => ({ count: state.count + 1 }),
+     decrement: (state) => ({ count: state.count - 1 }),
    },
  })
);

Примечание: если вы пишете на typescript, то код выше не требует никакой дополнительной анотации типов.

Payload в action следует передавать как параметр:


const summer = createForme(
  {
    sum: 0,
  },
  () => ({
    actions: {
      add: (state, payload) => ({ count: state.sum + payload }),
    },
  })
);

const store = initStore({
  formes: {
    counter,
    summer,
  },
});

Легко ли использовать counter?


Однозначно да. В forme есть поле actions и в нём синхронные действия. Чтобы вызвать их нужно лишь указать через dispatch имя forme и action'а:


store.dispatch.counter.increment();

store.dispatch.summer.add(100);

Теперь состояние стора обновилось и будет следующим:


store.state = {
  counter: {
    count: 1,
  },
  summer: {
    sum: 100,
  },
};

Mожно также присвоить increment переменной и вызывать как обычную функцию. Внутри statirjs работает на замыканиях, а не на контексте:


const increment = store.dispatch.counter.increment;

increment();

При использовании react доступ к dispatch'у осуществляется через хук:


import { useDispatch } from "@statirjs/react";

const increment = useDispatch((dispatch) => dispatch.counter.increment);



Запоминаем №2: экшены разбиты на компоненты, но есть возможность получить всё состояние как у redux


Запоминаем №3: statirjs активно использует замыкания и позволяет манипулировать экшенами как если бы они были простыми функциями


Запоминаем №4: statirjs поддерживает хуки




Как писать действия с внешними эффектами?


За эффекты отвечает поле pipes, которое как actions, но чуточку сложнее:


const asyncCounter = createForme(
  {
    count: 0,
  },
  () => ({
    pipes: {
      asyncIncrement: {
        push: async (state) => ({ count: state.count + 1 }),
      },
    },
  })
);

const store = initStore({
  formes: {
    asyncCounter,
  },
});

store.dispatch.asyncCounter.asyncIncrement();

Что здесь происходит?


  1. в фабрику createForme передаётся начальное состояние и функция;
  2. второй аргумент createForme (функция) возвращает объект с pipes;
  3. в pipes определен объект asyncIncrement;
  4. asyncIncrement содержит функцию push с небольшой задержкой;
  5. созданный asyncCounter передаётся в initStore для создания стора;
  6. asyncIncrement вызывается через dispatch для асинхронного обновления кода;



Запоминаем №5: эффекты можно писать с использованием стандартного async/await




Любая pipe как и action работает через замыкание и на практике является простой асинхронной функцией с соответствующей типизацией:


const increment = store.dispatch.asyncCounter.asyncIncrement;

await increment();

В чём сложность и отличие от actions?


Во-первых actions нужны только для синхронных действий, pipes наоборот. во-вторых, на самом деле, каждая pipe разделена на шаги push, core, done, fail для сторогсти контролирования этапов асинхронного действия:


const asyncCounter = createForme(
  {
    count: 0,
    isLoading: false,
  },
  () => ({
    pipes: {
      asyncIncrement: {
        push(state) {
          return { ...state, isLoading: true };
        },
        async core(state) {
          await someDelay();
          return state.count + 1;
        },
        done(state, payload, data) {
          return {
            count: data,
            isLoading: false,
          };
        },
        fail(state) {
          return { ...state, isLoading: false },
        },
      },
    },
  })
);

Разделение следующее: push вызывается первым (здесь могут располагаться подготовительные действия), core для выполнения основной работы pipe'ы, done выполняется при успехе, fail при ошибке. Разделение осуществляется за счёт использования try catch внутри pipe.




Запоминаем №6: pipe разделена на шаги


Запоминаем №7: pipe из коробки ловит ошибки




Взаимодействие formes


При разработке может возникнуть необходимость управлять состоянием связанно, вызывая из forme другую forme. Для этого можно воспользоваться dispatch в рамках createForme:


const asyncCounter = createForme(
  {},
+  (dispatch) => ({
    pipes: {
      asyncIncrement: {
        push() {
          dispatch.counter.increment();
        }
      },
    },
  })
);

Примечание: при необзодимости можно строить высокую иерархию зависимостей между formes, выделяя элементарные и управляющие forme.



Запоминаем №8: все formes связанны через dispatch объект




Как отслеживать изменения?


Если используете react, то через @statirjs/react hooks:


import { useSelect } from "@statirjs/react";

const count = useSelect((rootState) => rootState.counter.count);

Если используете только @statirjs/core, то подписку. Подписка вызывается на action, pipe:push, pipe:done и pipe:fail:


store.subscribe(console.log);

Плюсы


Получаем cледующие удобности и плюсы от использования statirjs:


  1. малый вес;
  2. actions — это чистые функции;
  3. используется компонентный подход;
  4. можно получать общее состояние как у redux;
  5. части frome можно переиспользовать;
  6. statirjs активно использует замыкания и позволяет манипулировать экшенами как если бы они были простыми функциями;
  7. redux-devtool из коробки;
  8. statirjs поддерживает хуки;
  9. эффекты можно писать с использованием стандартного async/await;
  10. pipe разделена на шаги;
  11. pipe из коробки ловит ошибки;
  12. все formes связанны через dispatch;

Заключение


При разработке statirjs я видел его как простой инстумент для простой работы. очевидно нет никаких "killer feature", но развивается идея простоты rematch. Уже готовы пакеты core, react, persist и в будущем планируется поддерживать vue и angular. Statirjs это удобный инструмент (думается мне), но также хорошее место чтобы начать контрибьютить в open source.


Имеется страница с документацией

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 31

    +2

    Боже, хотите простоты используйте mobx...

      –1
      Я считаю, что statirjs будет проще осваивать для людей, которые используют redux-like state manager'ы. Словарь терминов и их реализация переиспользуется.
        0
        Redux — сложная и перегруженная лишним система, вы лишь немного ее упростили. Пайпы, диспатчи, билдеры, возвращение целого объекта стора в каждом экшене в иммутабельном формате… Это не просто и смысл создания подобного очень туманен. Без проблем готов обстоятельно сравнить с observable-стором, в котором все решения будут выглядеть намного чище и проще.
          0
          Скажите пожалуйста, что может быть сложнее чем это??? Сразу же пример с локальным стейтом компонента и с глобальным стейтом приложения. Да ничего проще быть не может в принципе.
          class LocalState {
              @observable counter = 0;
              @observable title = 'title';
          
              incr = () => {
                  this.counter++;
              }
          
              handleTitleChange = (e) => {
                  this.title = e.target.value;
              }
          }
          
          class GlobalState {
              @observable counter = 0;
              @observable title = 'global title';
          
              incr = () => {
                  this.counter++;
              }
          
              handleTitleChange = (e) => {
                  this.title = e.target.value;
              }
          }
          
          const globalState = new GlobalState();
          
          export const App = observer(() => {
              const [ state ] = useState(() => new LocalState());
          
              return (
                  <div>
                      <h1>{state.title}</h1>
                      <h2>{globalState.title}</h2>
          
                      <div>Local Counter: {state.counter}</div>
                      <div>Global Counter: {globalState.counter}</div>
          
                      <div>
                          <button onClick={state.incr}>Local incr</button>
                          <button onClick={globalState.incr}>Global incr</button>
                      </div>
          
                      <div>
                          <input value={state.title} onChange={state.handleTitleChange} />
                          <input value={globalState.title} onChange={globalState.handleTitleChange} />
                      </div>
                  </div>
              );
          });
          

          Вот — codesandbox.io/s/interesting-snyder-v8k4o?file=/src/App.tsx
          0
          Только mobx + binding к react + mobx-state-tree ~ 50Kb gzip кода, а redux + react-redux ~ 8Kb gzip кода.

          Ну и в конце, автор предложил достаточно крутое решение, чтобы избавить redux от бойлерплейта, а вы снова про mobx. Зачем?)
            0
            Mobx в gzip весит 15кб. А обвязка к реакту — 1.8кб. Если версия реакта позволяет использовать хуки, то можно использовать официальный mobx-react-lite вместо mobx-react.

            автор предложил достаточно крутое решение, чтобы избавить redux от бойлерплейта

            Проблема в том, что тут чуть ли не каждый второй пост про стейт-менеджер это презентация своего велосипеда или обёртки над Redux:
            Организация reducer'а через стандартный класс
            Redux-symbiote — пишем действия и редьюсеры почти без боли
            Redux — пересмотр логики reducer'a и actions
            Оверинжинирг 80 уровня или редьсюеры: путь от switch-case до классов
            Люди решают проблемы, которые уже давно решены другими инструментами, проверенными временем. Поэтому появляются люди, рассказывающие об этих инструментах.
              0
              bundlephobia.com/result?p=mobx@5.15.4 ~15.4Kb
              bundlephobia.com/result?p=mobx-react@6.2.2 ~5KB
              bundlephobia.com/result?p=mobx-state-tree@3.16.0 ~ 20Kb
              И это только тот код, который позволит с mobx как-то удобно работать.

              В том то и прелесть, что можно как угодно организовать. Есть у redux проблема с boilerplate — есть решения. Сама концепция единого стора же простая.
                0
                Общий размер Mobx + mobx-react-lite на современном React — 15.4 + 1.8кб, а не 50. Ссылки на bundlephobia в посте выше. MST даёт дополнительные инструменты вроде рантайм проверки типов, нормализации из коробки и time travel, что вряд ли нужно в простом приложении.
                +1
                Я одной вещи не понимаю, зачем под каждой статьей про redux писать про mobx? Еще и с эмоциями типа «Боже....». Вы правда думаете, что есть еще те, кто не знает про mobx?
                  –2
                  Потому что нелепо в 2020 году до сих пор использовать redux-like дичь, или писать не реактивные стейт менеджеры тем более иммутабильные. Более того это нелепо было уже в 2016 году, просто не так ярко выражено.
                    –2
                    Знают, но большинство новых проектов стартуют на Redux, судя по тем собеседованиям, на которые хожу. Почему?
                    В свое время это был хайп, который здравомыслящие люди обходили стороной ввиду больших сложностей применения на практике, но пора бы уже подобным концептам уйти на покой и быть признанными нежизнеспособными. Однако разработчики продолжают по каким-то причинам мусолить эту тему, а Абрамов вместо того, чтобы признать, что создал стремную систему, еще и в реакт-хуки запихивает имплементацию. Как тут без эмоций?
                      0

                      Да, действительно. Есть такие. К сожалению, мой опыт относительно редакса негативный: приходилось работать с кодом, где редакс исковеркан и использован неправильно.
                      Redux-toolkit исправляет задачу. А вместе с normalizr мы приходим к почти mst.
                      Теперь в redux'е есть immer.js, от создателей mobx.


                      Сейчас, написав и участвовав в написании около 10 приложений, понимаю, что проще использовать mobx + mst. Чем Redux, чем стейт-менеджмент на хуках, и даже, чем redux-toolkit.


                      А рассматриваемая библиотека напоминает slice из redux-toolkit. Только у нее наверняка хуже документация и меньше ответов на stackoverflow. Не весь же код пишут сеньоры

                        0
                        Используя mst вы убиваете все приемущества и кайф от mobx'a. Зачем?????
                0
                Представим что нужно декрементировать значение. С statirjs это будет быстро и просто:
                +     decrement: (state) => ({ count: state.count - 1 })
                Быстро и просто это так:

                state.count--

                Аналогично для dispatch'а. Просто — это когда counter.increment(), а не хуки на все экшны:

                const increment = useDispatch((dispatch) => dispatch.counter.increment);

                  –1
                  1. желание иметь простой redux как rematch;


                  Используйте обычный react state.

                  Не буду писать про mobx, но у меня такое ощущение, что через пару лет, люди поймут, что это очень большой over engineering и наконец начнут использовать просто state и вспомогательные функции для преобразования, что то вроде rxjs.
                    –1
                    Не буду писать про mobx, но у меня такое ощущение, что через пару лет, люди поймут, что это очень большой over engineering и наконец начнут использовать просто state и вспомогательные функции для преобразования, что то вроде rxjs.

                    MobX оверинжинеринг? Это не более чем использование возможностей языка для максимального удобства в разработке. А пользоваться им настолько просто, что даже тапочек сложнее.
                    Вот пожалуйста — codesandbox.io/s/interesting-snyder-v8k4o?file=/src/App.tsx
                    Проще просто не может быть, изменяешь переменную и там где надо вызываются реакции на эти изменения, вот и всё.
                      –1

                      Вы пишите о совсем другом. Неважно как это используется, сложно или просто. Важны причины использования.

                        0
                        Неважно как это используется, сложно или просто. Важны причины использования.

                        В завтрашний день могут смотреть не только лишь все?)
                    0

                    Не в защиту redux like решений, но все же в mobx тоже свои особенности, основная это концепция мультисторов, что приводит к необходимости управления жизненным циклом этих сторов, и организации их взаимодействия друг с другом. Чего естественно нет в решениях с одним стором типа redux.

                      +1
                      Здравствуйте, а можно чуть подробнее? В редаксе приняты многочисленные одноуровневые сторы, а что вы подразумеваете под мультисторами мобх и жизненным циклом? Если это действительно выглядит проблемой, подумаю над решением
                        0

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


                        В mobx же для сторов используются классы для того чтобы сделать их поля observable, так как это классы, то для их использования необходимо создавать инстансы сторов, как только вы начинаете работать с классами и инстансами у вас возникает вопрос правильной последовательности создания этих инстансов и всякие dependency injection и тп штуки.

                          –3
                          то для их использования необходимо создавать инстансы сторов, как только вы начинаете работать с классами и инстансами у вас возникает вопрос правильной последовательности создания этих инстансов и всякие dependency injection и тп штуки.

                          1) Накой собвственно вам «всякие dependency injection и тп штуки»?? Они как бы нафиг не нужны.
                          2) Причем тут «правильная» последовательность? Если вы планируете что сторы будут зависить на прямую друг от друга и в конструкторе вызывать свойства и методы друг друга, то это не правильный дизайн, но если все же такая необходимость нужна, это решается разными методами, например в конструкторе класса которые перекрестно используют друг друга дергать методы только после setTimeout, чтобы они оба на момент дергаться были инициализированы и не получилось так, что кто-то из них пока что-то undefined, или же создается промежуточный класс или функция, которая импортирует эти 2 класса и потом дергает их нужные методы и свойства.
                            0
                            По сути просто не нужно обращаться к соседним сторам в constructor, то есть сначала создается весь стор целиком, а потом вызываются методы. Так не будет никаких undefined, и setTimeout не пригодится) Так что тоже не считаю это проблемой
                              0
                              Я же написал
                              Если вы планируете что сторы будут зависить на прямую друг от друга и в конструкторе вызывать свойства и методы друг друга, то это не правильный дизайн

                              А потом
                              но если все же такая необходимость нужна
                                0
                                Ну да, я лишь дополнил варианты в случае, когда «необходимость нужна», противоречий нет)
                            0
                            В редаксе действительно 1 стор, но состоит он из стольких «подсторов» с редюсерами, сколько нужно приложению:

                            import { combineReducers } from "redux";
                            
                            import store1 from "./store1";
                            import store2 from "./store2";
                            // + 100500 других сторов
                            
                            export default combineReducers({
                              store1,
                              store2,
                            });
                            

                            В MobX тоже можно комбинировать «подсторы»:

                            import { Store1 } from './Store1 ';
                            import { Store2 } from './Store2 ';
                            
                            export class StoreRoot {
                              constructor() {
                                this.store1 = new Store1();
                                this.store2 = new Store2();
                              }
                            }
                            

                            То есть по способу компоновки сторов очень похоже. Вы, наверное, имеете в виду ситуацию, когда Store1 должен обратиться к Store2? Это решается несложно. А в редаксе другая схема работы — в нем сторы вообще не могут общаться, они просто содержат данные, а экшены могут обращаться с помощью глобальных констант к любому стору. Можно сделать такую же схему и на MobX, он никак не ограничивает возможности.
                              0

                              Хочу обратить ваше внимание, что ststirjs позволяет взаимодействовать между formes (считай подсторомами) через dispatch внутри самих форм.

                                +1
                                Хочу обратить ваше внимание, что с MobX вы просто изменяете свойства объекта/класса и всё работает.
                                this.isFetching = true;
                                const response = await apiRequest(`GET /items`);
                                this.items = response.items; 
                                this.isFetching = false;
                                

                                И всё! Я надеюсь вы понимаете что ваше решения мягко говоря гораздо более громоздкое, неудобное, не использует возможности языка и т.д и т.п.
                                Вы как будто специально игнорируете то, что в JS уже очень давно есть Proxy и Getters/Setters (если нужна поддержка совсем древних браузеров).
                                Игнорировать их и не пользоваться этим благом просто абсурдно и смешно, тем более в 2020 году, хотя этим уже много лет активно пользуются, но мазохисты конечно же об этом либо не знают, либо отрицают.
                        0

                        Мы все делаем вид, что unstated-next не существует, я правильно понимаю?

                          –4
                          А с чего вдруг «это творение» должно существовать для кого-то? Оно точно такое же как и все остальные подобные «творения», которые так же ни для кого не существуют по понятным причинам.
                            0

                            Интересное решение, но statirjs предполагает независимое от фреймворков ядро с коннекторами, а unstated-next нет. Преимуществом может являться нативность unstated-next относительно react, но это повышает связанность с ним же (насколько сильно поменяется react api, настолько же и весь unstated-next в целом). Но в целом любопытный проект.

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

                          Самое читаемое