Аннотация
В фронтэенде многие предпочитают (или хотели бы) использовать лёгкие и простые пакеты. Кроме того, на текущий момент использовать средства управления состоянием — это стандарт. Я постарался объединить эти принципы и сделать новый state manger — statirjs. Идеологической основой послужили: rematch, redux.
Цель статьи
Дать краткий обзор основному функционалу statirjs. Без сравнений и лишней теории.
Область применения
Основной областью применения statirjs являются малые/личные проекты, которым не требуются: многочисленные внешние зависимости, повышенный теоретический порог для использования средства управления состоянием.
Причины создания
- желание иметь простой redux как rematch;
- перенасыщенность выделенными сущностями в redux — статья;
- стремление к отсутствию внешних зависимостей;
- необходимость независимости платформы для её развития;
- стремление к малому размеру;
Основные плюсы 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,
},
});
Что здесь происходит?
- в фабрику 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,
},
});
Легко ли использовать 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();
Что здесь происходит?
- в фабрику 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?
Во-первых 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.
AVSDoom
Боже, хотите простоты используйте mobx...
bre30krs69cs Автор
Я считаю, что statirjs будет проще осваивать для людей, которые используют redux-like state manager'ы. Словарь терминов и их реализация переиспользуется.
DmitryKazakov8
Redux — сложная и перегруженная лишним система, вы лишь немного ее упростили. Пайпы, диспатчи, билдеры, возвращение целого объекта стора в каждом экшене в иммутабельном формате… Это не просто и смысл создания подобного очень туманен. Без проблем готов обстоятельно сравнить с observable-стором, в котором все решения будут выглядеть намного чище и проще.
MaZaAa
Скажите пожалуйста, что может быть сложнее чем это??? Сразу же пример с локальным стейтом компонента и с глобальным стейтом приложения. Да ничего проще быть не может в принципе.
Вот — codesandbox.io/s/interesting-snyder-v8k4o?file=/src/App.tsx
artemmalko
Только mobx + binding к react + mobx-state-tree ~ 50Kb gzip кода, а redux + react-redux ~ 8Kb gzip кода.
Ну и в конце, автор предложил достаточно крутое решение, чтобы избавить redux от бойлерплейта, а вы снова про mobx. Зачем?)
kubk
Mobx в gzip весит 15кб. А обвязка к реакту — 1.8кб. Если версия реакта позволяет использовать хуки, то можно использовать официальный mobx-react-lite вместо mobx-react.
Проблема в том, что тут чуть ли не каждый второй пост про стейт-менеджер это презентация своего велосипеда или обёртки над Redux:
— Организация reducer'а через стандартный класс
— Redux-symbiote — пишем действия и редьюсеры почти без боли
— Redux — пересмотр логики reducer'a и actions
— Оверинжинирг 80 уровня или редьсюеры: путь от switch-case до классов
Люди решают проблемы, которые уже давно решены другими инструментами, проверенными временем. Поэтому появляются люди, рассказывающие об этих инструментах.
artemmalko
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 — есть решения. Сама концепция единого стора же простая.
kubk
Общий размер Mobx + mobx-react-lite на современном React — 15.4 + 1.8кб, а не 50. Ссылки на bundlephobia в посте выше. MST даёт дополнительные инструменты вроде рантайм проверки типов, нормализации из коробки и time travel, что вряд ли нужно в простом приложении.
artemmalko
Я одной вещи не понимаю, зачем под каждой статьей про redux писать про mobx? Еще и с эмоциями типа «Боже....». Вы правда думаете, что есть еще те, кто не знает про mobx?
MaZaAa
Потому что нелепо в 2020 году до сих пор использовать redux-like дичь, или писать не реактивные стейт менеджеры тем более иммутабильные. Более того это нелепо было уже в 2016 году, просто не так ярко выражено.
DmitryKazakov8
Знают, но большинство новых проектов стартуют на Redux, судя по тем собеседованиям, на которые хожу. Почему?
В свое время это был хайп, который здравомыслящие люди обходили стороной ввиду больших сложностей применения на практике, но пора бы уже подобным концептам уйти на покой и быть признанными нежизнеспособными. Однако разработчики продолжают по каким-то причинам мусолить эту тему, а Абрамов вместо того, чтобы признать, что создал стремную систему, еще и в реакт-хуки запихивает имплементацию. Как тут без эмоций?
Carduelis
Да, действительно. Есть такие. К сожалению, мой опыт относительно редакса негативный: приходилось работать с кодом, где редакс исковеркан и использован неправильно.
Redux-toolkit исправляет задачу. А вместе с
normalizr
мы приходим к почти mst.Теперь в redux'е есть immer.js, от создателей mobx.
Сейчас, написав и участвовав в написании около 10 приложений, понимаю, что проще использовать mobx + mst. Чем Redux, чем стейт-менеджмент на хуках, и даже, чем redux-toolkit.
А рассматриваемая библиотека напоминает slice из redux-toolkit. Только у нее наверняка хуже документация и меньше ответов на stackoverflow. Не весь же код пишут сеньоры
MaZaAa
Используя mst вы убиваете все приемущества и кайф от mobx'a. Зачем?????