Всем привет! На связи Supercat и я хочу рассказать о менеджере состояний Supercat Store.
Supercat Store - это JavaScript-библиотека, которая позволяет легко отслеживать и реагировать на изменения стейта приложения или его части.
Коротко о Supercat Store:
Легковесная: 12 kB - minified, 3.8 kB - minified + gzipped;
Не зависит от фреймворков, агностик;
Можно создавать столько сторов, сколько нужно;
Использует мутабельную систему реактивности;
Применяются ленивые вычисления для computed;
Поддержка мгновенных и отложенных реакций на изменения состояния;
Код типизирован с помощью TypeScript внутри JSDoc;
Код документирован, в документации почти на каждый метод есть пример использования;
Лицензия MIT.
Кирпичики реактивности
Перед тем как показывать примеры использования, я коротко введу в курс дела об архитектуре.
Каждый экземпляр Store создает реактивные элементы, за изменениями которых вы можете наблюдать. Они делятся на три типа:
Atom - базовый тип. Как правило, содержит примитивы или небольшие объекты.
Collection - наблюдаемые массивы. С помощью коллекций легко отслеживать изменение элементов массива и его длины.
Computed - элемент, значение которого функционально зависит от Atom или Collection.
На изменения каждого отдельного элемента можно подписаться с помощью метода subscribe и вызвать так называемую реакцию на изменение. И я думаю, что для начала работы этой теории вполне хватит.
Установка
Самый удобный способ установки для NodeJs - через npm (не забудьте добавить в package.json секции "type": "module"
и "moduleResolution": "nodenext"
):
npm install @supercat1337/store
После чего доступна возможность создания Store:
import { Store } from "@supercat1337/store";
Если не используете бандлеры и хотите работать с библиотекой напрямую, то укажите подключение модуля по url:
import { Store } from "https://cdn.jsdelivr.net/npm/@supercat1337/store@latest/dist/store.bundle.esm.js";
Стартуем
Давайте напишем базовый пример - создание Computed, значение которого равно сумме значений атомов "a" и "b".
import { Store } from "@supercat1337/store";
var store = new Store;
var a = store.createAtom(0);
var b = store.createAtom(0);
var c = store.createComputed(() => {
return a.value + b.value;
});
c.subscribe(() => {
console.log("c = ", c.value);
});
В этом примере мы видим, что атомы "a" и "b" инициализированы со значением 0. Для создания Computed используется метод createComputed. Можно заметить, что мы не использовали ручные подписки на атомы, поэтому выглядит довольно лаконично. Важно отметить, что подписка происходит только на те атомы и коллекции, которые созданы одним экземпляром Store.
Теперь при изменении "a" или "b" будет автоматически меняться значение "c", а при изменении значения "c", в консоли отобразится ее значение. Проведем простые манипуляции и увидим результат в консоли.
a.value++;
// outputs: c = 1
b.value++;
// outputs: c = 2
Для того, чтобы реакции не вызывались так часто, есть несколько способов. Это очень важно, когда вы хотите реактивную переменную связать с DOM-элементом. Рассмотрим, парочку.
Первый способ - использование отложенной реакции. Для этого просто добавим число миллисекунд (debounce_time) при вызове метода subscribe:
c.subscribe(() => {
console.log("c = ", c.value);
}, 100);
Теперь реакция будет вызваться ровно 1 раз на все изменения элемента в течение 100 мс.
a.value++;
b.value++;
// outputs: c = 2
Это все работает, но для кого-то будет более правильным подписку оформлять таким образом:
c.subscribe((details) => {
console.log("c = ", details.value);
}, 100);
В объекте details передаются важные данные, одни из которых - это значение наблюдаемого реактивного элемента в момент вызова реакции. Это может быть важно тем, кто не хочет в реакции работать с текущим значением элемента, а с его "историческим". Представьте, что отложенная ваша реакция вызывается через секунду. Вполне можно задаться логичным вопросом: "А какое значение атома было в тот момент времени?".
Второй способ оптимизации количества вызовов реакций - использование "пакетного режима" установки значений. То есть сначала устанавливаем значения, а только потом после вызываются реакции на изменения всех затронутых элементов. Для этого придется немного изменить код:
import { Store } from "@supercat1337/store";
var store = new Store;
var a = store.createAtom(0, "a");
var b = store.createAtom(0, "b");
var c = store.createComputed(() => {
return a.value + b.value;
});
c.subscribe(() => {
console.log("c = ", c.value);
});
store.setItems({ a: 1, b: 1 });
// outputs: c = 2
Мы добавили имена для наших атомов. Указание имен дает дополнительный функционал для работы со стором. Один из них - это пакетная установка значений через метод setItems.
К примеру, зная имена элементов стора, можно получать объекты Atom, Computed, Collection из нужного экземпляра Store, используя методы store.getAtom("имя_элемента"), store.getComputed("имя_элемента"), store.getCollection("имя_элемента") соответственно.
Пример работы с массивами
Работать с массивами тоже крайне просто
import { Store } from "@supercat1337/store";
var store = new Store;
var a = store.createCollection([]);
a.subscribe((details) => {
console.log(`${details.eventType}: arr["${details.property}"] = ${details.value}`);
});
var arr = a.value;
arr.push(1);
// set: arr["length"] = 1
// set: arr["0"] = 1
arr.push(2);
// set: arr["length"] = 2
// set: arr["1"] = 2
Здесь нам помогает в работе объект details, о котором мы упоминали выше. Вот его важные свойства:
eventType - тип события, принимает значение "set" или "delete";
property - имя измененного свойства массива. Для Atom и Computed будет равен null.
value - значение измененного свойства.
А теперь попробуем создать Computed для отслеживания изменения длины массива:
import { Store } from "@supercat1337/store";
var store = new Store;
var a = store.createCollection([]);
var b = store.createComputed(()=>{
return a.value.length;
});
b.subscribe((details) => {
console.log(`b = ${details.value}`);
});
var arr = a.value;
arr.push(1);
// outputs: b = 1
arr.push(2);
// outputs: b = 2
arr.pop()
// outputs: b = 1
В одной публикации весь функционал библиотеки не осветить. Однако, я хочу дополнить статью еще одним частым кейсом: когда уже имеешь объект и просто хочешь добавить к нему реактивность.
Обертка объекта
// @ts-check
import { Store } from "@supercat1337/store";
var store = new Store;
class SampleClass {
a = 0;
c = [];
incA () {
this.a++;
}
}
var sample = store.observeObject(new SampleClass);
sample.store.subscribe("a", (details)=>{
// a is changed
//store.log(details);
});
sample.store.subscribe("c", (details)=>{
// c is changed
//store.log(details);
});
sample.incA();
sample.incA();
sample.c.push("foo");
Теперь sample имеет дополнительное свойство store, через которое можно отслеживать, добавлять computed и использовать прочий функционал. И самое главное, что сохранился тип объекта, что повышает удобство кодинга.
Ограничения
Как мы знаем, любое техническое решение всегда является компромиссом. В процессе разработки библиотеки я не хотел раздувать код, при этом добивался цели вместить туда тот функционал, который мне нужен на практике наиболее часто.
По этой причине я также хочу указать на наличие ограничений:
Computed всегда будут вычисляться от атомов или коллекций, даже если в коде вы укажете только Computed. На этой библиотеке Excel не построить, пока что.
Нет глубокого отслеживания изменения объектов. Если значение атома - объект, то не забывайте прямо указывать присваивание свойству value нового значения.
Вытекает из п. 2. Если элемент массива является объектом, то в случае изменения значения свойства объекта, изменение не будет зафиксировано. Поэтому необходимо в коде прямо указывать операцию присваивания: a.value[0] = {foo: 1};
Итоги
Supercat Store - это легкий стейт-менеджер, который позволяет писать лаконичный и читабельный код. Буду рад вашим комментариям, конструктивным идеям и предложениям. В следующих публикациях я хочу рассказать, как можно использовать эту библиотеку для управления DOM.
Если у вас возникнут вопросы, то смогу ответить в комментариях, а также в группе https://t.me/super_cat_dev.
Комментарии (31)
DarthVictor
18.05.2024 06:36+1Когда уже нативные сигналы привезут в браузеры?
SuperCat911 Автор
18.05.2024 06:36Во-первых, явно нескоро.
Во-вторых, потом опять пойдет волна библиотек, которые будут их оборачивать. Например, будут писать обновление данных одним пакетом и отложенные реакции. И будут думать, как выстраивать порядок выполнения реакций (эффектов).
DarthVictor
18.05.2024 06:36+1Никто сейчас в здравом уме не оборачивает нативные Промисы или методы коллекций для массивов.
Более того, многие библиотеки стали выкидывать после перехода нативных методов просто в Stage3, потому что разницы по производительности между полифилом и рандомной библиотекой нет, а у полифила хотя бы не находят каждый год фатального недостатка в API.
gmtd
18.05.2024 06:36+1Промисы просты как валенок
Если посмотреть на fine-grained Reactivity API Vue, то просто сигналов однозначно мало
Реактивность примитивов, объектов, массивов, глубокая и shallow, scopes
SuperCat911 Автор
18.05.2024 06:36Честно говоря, не понял причем тут обёртка методов и промисов. Оборачивают объекты и строят абстракции над объектами для того, чтобы добавлять функционал.
А что касается тех сигналов, то там пока Stage1. Говорить рано ещё о них.
ponikrf
18.05.2024 06:36Непонятно зачем минусуют люди. Исходники есть, можно все глянуть. Комментарии в исходнике вполне себе нормальные. Может из-за позиционирования?
Мне вот иногда нужен простой стейт менеджер на бэке, дак днем с огнем что то простое не сыщешь.
Единственное что бросается в глаза - очень длинные названия методов и многословность.
AccountForHabr
18.05.2024 06:36+1Имхо сравнивают с mobx и понимают что mobx лучше.
SuperCat911 Автор
18.05.2024 06:36Пока в комментариях видел только один пример с MobX, и то, всю красоту работы MobX спрятали во внешнюю функцию observer - адаптер для реакта.
markelov69
18.05.2024 06:36+3Если честно, то это вообще полная фигня. Просто посмотрите на MobX, он на 3 головы лучше и удобнее. Там все подписки/отписки автоматические, как и должно быть. Из "оверхеда" по коду только makeAutoObservable(this); в конструкторе классов с состоянием и observer() для заворачивая компонентов. Всё.
Просто посмотрите https://stackblitz.com/edit/vitejs-vite-y2qj7g?file=src%2FApp.tsx&terminal=devAccountForHabr
18.05.2024 06:36Он не прибит к реакту. Посмотрите примеры из самого mobx.
SuperCat911 Автор
18.05.2024 06:36Я знаю эту библиотеку. Но то, что указано выше - это биндинг данных к JSX реакта. Читайте как демонстрация работы внешней функции observer, а не то как библиотека работает с данными.
Если бы пример был более нативный, то тогда было бы интереснее сравнить.
AccountForHabr
18.05.2024 06:36А вас есть пример делающий тоже самое?
SuperCat911 Автор
18.05.2024 06:36В публикации речь же идет о ядре. А адаптеры для фреймворков - все-таки отдельные проекты. Для реакта потом напишу. Там тоже будет одна функция.
Zukomux
18.05.2024 06:36+1Т.е. либа агностик, но для реакта нет реализации. Понятно. А в чем принципиальное отличие от Zustand если вам MobX не угодил?
SuperCat911 Автор
18.05.2024 06:36Разъясните противопоставление "агностик" и "для реакта нет реализации".
markelov69
18.05.2024 06:36+1Если бы пример был более нативный, то тогда было бы интереснее сравнить.
Вот пожалуйста, самый нативный пример https://stackblitz.com/edit/vitejs-vite-wgaaja?file=src%2Fmain.ts&terminal=dev
SuperCat911 Автор
18.05.2024 06:36Да, конечно. Другое дело.
Вот мой ответ:
https://stackblitz.com/edit/vitejs-vite-u7mvue?file=main.js&terminal=dev
И кстати, мой код более читабельнее.
А when все-таки компьютед по духу.
AccountForHabr
18.05.2024 06:36+1Вы упростили пример - нету кейса когда необходимо делать реакцию на изменения 2 свойств. судить о читабельности - рано кмк.
Необходимо явно подписываться на изменения свойств - при рефакторинге, изменении требований, etc - легко забыть подписаться на нужное свойство/отписаться от ненужного. Да и не хочется этим заниматься.
В коде используются магические константы - при рефакторинге/наборе можно перепутать.
SuperCat911 Автор
18.05.2024 06:36Можно и через атомы. Нет проблем.
https://stackblitz.com/edit/vitejs-vite-nkjk5v?file=main.js&terminal=dev
А то, что реакции явно указываются, так это хорошо. Это улучшает читаемость кода.
19Zb84
18.05.2024 06:36+1var a = store.createAtom(0, "a"); var b = store.createAtom(0, "b");
Я правильно понимаю, что она на основе атомарных операций построена ?
Вроде прикольная библиотека.
Мне кажется я что то подобное сейчас использую, только я взял за основу freeQueue от гугла.SuperCat911 Автор
18.05.2024 06:36Да, все верно.
19Zb84
18.05.2024 06:36Атомарные операции это единственный адекватный путь к многопоточности.
О чем тогда все комментарии выше ?
Там у людей видимо ЧСВ высокое, а знания нулевые.
adminNiochen
18.05.2024 06:36+1Я человек простой - вижу var в js коде, дальше не читаю...
SuperCat911 Автор
18.05.2024 06:36Конечно, не надо! И когда в следующий раз увидите var, обязательно сообщите это читателям.
aybekkun
Хотелось бы увидит как оно будет использоваться внутри react
Keeper10
Никак, учитывая, что
SuperCat911 Автор
А что вы собрались оборачивать? Virtual DOM что ли? Вообще, манипуляции с DOM (обновление) должны быть внутри реакции, неважно какую библиотеку рендеринга вы используете в работе.
Zukomux
Ну действительно, при использовании других стейт менеджеров в реакте же оборачивают vdom