Supercat Store — новый менеджер состояний на JavaScript
Всем привет! На связи 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.