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.


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