[Из песочницы] Простой state manager для простой работы
В фронтэенде многие предпочитают (или хотели бы) использовать лёгкие и простые пакеты. Кроме того, на текущий момент использовать средства управления состоянием — это стандарт. Я постарался объединить эти принципы и сделать новый state manger — statirjs. Идеологической основой послужили: rematch, redux.
Дать краткий обзор основному функционалу statirjs. Без сравнений и лишней теории.
Основной областью применения statirjs являются малые/личные проекты, которым не требуются: многочисленные внешние зависимости, повышенный теоретический порог для использования средства управления состоянием.
- желание иметь простой redux как rematch;
- перенасыщенность выделенными сущностями в redux — статья;
- стремление к отсутствию внешних зависимостей;
- необходимость независимости платформы для её развития;
- стремление к малому размеру;
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,
},
});
Что здесь происходит?
- в фабрику createForme передаётся начальное состояние и функция;
- второй аргумент createForme (функция) возвращает объект с actions;
- в actions определена функция increment;
- increment получает состояние forme counter до вызова и после выполнения возвращает новое, следующее состояние;
- созданный 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,
},
});
Однозначно да. В 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();
Что здесь происходит?
- в фабрику createForme передаётся начальное состояние и функция;
- второй аргумент createForme (функция) возвращает объект с pipes;
- в pipes определен объект asyncIncrement;
- asyncIncrement содержит функцию push с небольшой задержкой;
- созданный asyncCounter передаётся в initStore для создания стора;
- asyncIncrement вызывается через dispatch для асинхронного обновления кода;
Запоминаем №5: эффекты можно писать с использованием стандартного async/await
Любая pipe как и action работает через замыкание и на практике является простой асинхронной функцией с соответствующей типизацией:
const increment = store.dispatch.asyncCounter.asyncIncrement;
await increment();
Во-первых 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:
- малый вес;
- actions — это чистые функции;
- используется компонентный подход;
- можно получать общее состояние как у redux;
- части frome можно переиспользовать;
- statirjs активно использует замыкания и позволяет манипулировать экшенами как если бы они были простыми функциями;
- redux-devtool из коробки;
- statirjs поддерживает хуки;
- эффекты можно писать с использованием стандартного async/await;
- pipe разделена на шаги;
- pipe из коробки ловит ошибки;
- все formes связанны через dispatch;
При разработке statirjs я видел его как простой инстумент для простой работы. очевидно нет никаких «killer feature», но развивается идея простоты rematch. Уже готовы пакеты core, react, persist и в будущем планируется поддерживать vue и angular. Statirjs это удобный инструмент (думается мне), но также хорошее место чтобы начать контрибьютить в open source.
Имеется страница с документацией