devlink-statecore
v1.0.3
Published
Minimalist framework-agnostic state manager for JavaScript/TypeScript. Zero dependencies, immutable updates, middleware support, computed state. Works in any JS runtime.
Downloads
37
Readme
DevLink StateCore
Минималистичный фреймворк-агностичный state-менеджер для JavaScript и TypeScript.
Ноль зависимостей. Иммутабельные обновления. Middleware. Computed state. Работает везде.
Содержание
Возможности
- Фреймворк-агностичность — работает в любом JS-окружении: браузер, Node.js, Deno, Bun
- Ноль зависимостей — ядро не использует внешних библиотек
- TypeScript из коробки — полная типобезопасность, все типы экспортируются
- Иммутабельное состояние —
Object.freezeпри каждом обновлении - Middleware — Redux-подобный compose pattern для логирования, персистенции, devtools
- Computed/derived state — ленивое вычисление с кешированием зависимостей
- Батчинг уведомлений — несколько синхронных обновлений вызывают один listener
- Селекторы — подписка на конкретный срез состояния, без лишних перерисовок
- Компактность — ядро ~200 строк, минифицированный бандл ~1.8 KB
Установка
npm install devlink-statecoreБыстрый старт
import { createStore } from "devlink-statecore";
// 1. Создаём стор
const store = createStore({
count: 0,
user: "anonymous",
});
// 2. Подписываемся на изменения
const unsubscribe = store.subscribe((state, prevState) => {
console.log("Было:", prevState.count, "→ Стало:", state.count);
});
// 3. Обновляем состояние
store.setState({ count: 1 });
store.setState((s) => ({ count: s.count + 1 }));
// 4. Читаем состояние
console.log(store.getState()); // { count: 2, user: "anonymous" }
// 5. Отписываемся
unsubscribe();API ядра
createStore<T>(initialState, options?)
Создаёт новый стор. Это единственная фабрика — все остальные методы доступны через возвращённый объект.
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| initialState | T | Начальное состояние (объект). Будет заморожен через Object.freeze |
| options?.name | string | Имя стора. Используется в ошибках и devtools. По умолчанию "DevLinkStore" |
| options?.middleware | Middleware<T>[] | Начальная цепочка middleware |
Возвращает: Store<T> — объект стора со всеми методами.
import { createStore } from "devlink-statecore";
// Без опций
const store = createStore({ count: 0, name: "test" });
// С опциями
import { logger } from "devlink-statecore/middleware";
const store = createStore(
{ count: 0, name: "test" },
{
name: "AppStore",
middleware: [logger()],
},
);store.getState()
Возвращает текущее состояние стора. Состояние заморожено (Object.freeze) — любая попытка мутации выбросит ошибку.
Параметры: нет.
Возвращает: Readonly<T> — замороженный объект текущего состояния.
const state = store.getState();
console.log(state.count); // 0
// Мутация выбросит TypeError
state.count = 5; // TypeError: Cannot assign to read only propertystore.setState(updater)
Обновляет состояние стора. Поддерживает два формата: частичный объект или функцию-updater. Выполняет shallow merge с текущим состоянием. Если результат shallow-equal текущему — обновление пропускается.
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| updater | Partial<T> | Частичный объект, поля которого будут merged в текущее состояние |
| updater | (state: T) => Partial<T> | Функция, принимающая текущее состояние и возвращающая partial-обновление |
Возвращает: void.
Выбрасывает: Error, если стор уничтожен (destroy()).
// Обновление через partial-объект
store.setState({ count: 10 });
// Обновление через функцию (имеет доступ к актуальному state)
store.setState((state) => ({ count: state.count + 1 }));
// Можно обновлять несколько полей за раз
store.setState({ count: 5, name: "updated" });
// Shallow-equal обновление пропускается (listeners не вызываются)
store.setState({ count: 0 }); // если count уже 0 — no-opstore.subscribe(listener, selector?)
Подписывает listener на изменения состояния. Нотификации батчатся через queueMicrotask: несколько синхронных setState вызовут listener один раз.
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| listener | (state: T, prevState: T) => void | Функция-обработчик. Получает новое и предыдущее состояние |
| selector | (state: T) => R (необязательный) | Функция-селектор. Если передана, listener вызывается только когда значение selector(state) изменилось (сравнение через Object.is) |
Возвращает: () => void — функция отписки. Вызов idempotent.
Выбрасывает: Error, если стор уничтожен (destroy()).
// Подписка на все изменения
const unsub = store.subscribe((state, prevState) => {
console.log("Было:", prevState, "→ Стало:", state);
});
// Подписка с селектором — вызывается только при изменении count
const unsub2 = store.subscribe(
(state, prevState) => {
console.log("count изменился:", prevState.count, "→", state.count);
},
(s) => s.count,
);
// Батчинг: 3 setState → 1 вызов listener
store.setState({ count: 1 });
store.setState({ count: 2 });
store.setState({ count: 3 });
// listener получит: prevState.count === 0, state.count === 3
// Отписка
unsub();
unsub2();store.use(middleware)
Динамически подключает middleware после создания стора. Middleware встраивается в конец существующей цепочки.
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| middleware | Middleware<T> | Функция middleware (формат: (api) => (next) => (updater) => void) |
Возвращает: void.
Выбрасывает: Error, если стор уничтожен (destroy()).
import { logger } from "devlink-statecore/middleware";
const store = createStore({ count: 0 });
// Подключаем middleware после создания
store.use(logger({ name: "Late" }));
store.setState({ count: 1 }); // middleware теперь активенstore.computed(name, definition)
Регистрирует вычисляемое (derived) значение. Значение ленивое — вычисляется только при вызове геттера. Результат кешируется до тех пор, пока зависимости не изменятся (сравнение через Object.is).
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| name | string | Уникальное имя computed-значения |
| definition.deps | ((state: T) => unknown)[] | Массив селекторов-зависимостей. Каждый извлекает одну зависимость из state |
| definition.compute | (...args: unknown[]) => R | Функция вычисления. Получает текущие значения зависимостей в том же порядке |
Возвращает: () => R — геттер-функция. Каждый вызов проверяет deps и при необходимости пересчитывает.
const store = createStore({ price: 100, quantity: 3, tax: 0.2 });
// Одна зависимость
const doubled = store.computed("doubled", {
deps: [(s) => s.price],
compute: (price) => (price as number) * 2,
});
console.log(doubled()); // 200 (вычислено)
console.log(doubled()); // 200 (из кеша, compute не вызван)
store.setState({ price: 50 });
console.log(doubled()); // 100 (пересчитано)
// Несколько зависимостей
const total = store.computed("total", {
deps: [(s) => s.price, (s) => s.quantity, (s) => s.tax],
compute: (price, qty, tax) => {
const subtotal = (price as number) * (qty as number);
return subtotal + subtotal * (tax as number);
},
});
console.log(total()); // 360
// Без зависимостей — вычисляется один раз и кешируется навсегда
const version = store.computed("version", {
deps: [],
compute: () => "1.0.0",
});store.destroy()
Уничтожает стор: очищает все подписки, middleware, computed-записи. После вызова setState, subscribe, use будут выбрасывать ошибку. getState продолжает возвращать последнее состояние. Повторный вызов destroy безопасен (idempotent).
Параметры: нет.
Возвращает: void.
const store = createStore({ count: 0 });
const unsub = store.subscribe(() => console.log("update"));
store.destroy();
store.getState(); // { count: 0 } — работает
store.setState({ count: 1 }); // Error: [DevLinkStore] Store is destroyed
store.subscribe(() => {}); // Error: [DevLinkStore] Store is destroyedMiddleware
Middleware перехватывает вызовы setState. Формат — Redux-подобная тройная стрелка: (api) => (next) => (updater) => void.
Импорт встроенных middleware:
import { logger, devtools, persist } from "devlink-statecore/middleware";logger(options?)
Логирует каждое обновление состояния в консоль: предыдущее и новое состояние, сгруппированные в console.group.
Параметры:
| Опция | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| collapsed | boolean | true | true — console.groupCollapsed, false — console.group |
| name | string | "DevLink" | Префикс в заголовке группы |
import { logger } from "devlink-statecore/middleware";
const store = createStore(
{ count: 0 },
{ middleware: [logger({ name: "MyApp", collapsed: false })] },
);
store.setState({ count: 1 });
// Консоль:
// ▼ [MyApp] state update
// prev state: { count: 0 }
// next state: { count: 1 }devtools(options?)
Записывает полную историю состояний в globalThis.__DEVLINK_DEVTOOLS__. Каждая запись содержит snapshot состояния, timestamp и тип действия.
Параметры:
| Опция | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| name | string | "DevLinkStore" | Ключ стора в devtools |
| maxHistory | number | 50 | Максимум записей в истории. Старые удаляются (FIFO) |
Структура globalThis.__DEVLINK_DEVTOOLS__:
{
stores: Map<string, {
history: Array<{
state: T;
timestamp: number;
action: "@@INIT" | "partial update" | "functional update";
}>;
currentIndex: number;
}>;
}import { devtools } from "devlink-statecore/middleware";
const store = createStore(
{ count: 0 },
{ middleware: [devtools({ name: "Counter", maxHistory: 100 })] },
);
store.setState({ count: 1 });
store.setState((s) => ({ count: s.count + 1 }));
// Просмотр истории
const dt = globalThis.__DEVLINK_DEVTOOLS__;
const history = dt?.stores.get("Counter")?.history;
console.log(history);
// [
// { state: { count: 0 }, action: "@@INIT", timestamp: ... },
// { state: { count: 1 }, action: "partial update", timestamp: ... },
// { state: { count: 2 }, action: "functional update", timestamp: ... },
// ]persist(options)
Сохраняет состояние в storage (по умолчанию localStorage) при каждом обновлении. При создании стора восстанавливает ранее сохранённое состояние.
Параметры:
| Опция | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| key | string | (обязательный) | Ключ в storage |
| storage | Storage | localStorage | Объект storage (localStorage, sessionStorage, кастомный). В Node.js/SSR — undefined (не падает) |
| serialize | (state: T) => string | JSON.stringify | Функция сериализации |
| deserialize | (raw: string) => Partial<T> | JSON.parse | Функция десериализации |
| partialize | (state: T) => Partial<T> | undefined | Если задана, сохраняет только указанные поля |
import { persist } from "devlink-statecore/middleware";
// Базовое использование
const store = createStore(
{ count: 0, token: "secret" },
{ middleware: [persist({ key: "app-state" })] },
);
// Сохранять только count (без token)
const store2 = createStore(
{ count: 0, token: "secret" },
{
middleware: [
persist({
key: "app-safe",
partialize: (state) => ({ count: state.count }),
}),
],
},
);
// sessionStorage вместо localStorage
const store3 = createStore(
{ count: 0 },
{
middleware: [
persist({ key: "session-data", storage: sessionStorage }),
],
},
);Свой middleware
Middleware — функция с сигнатурой (api) => (next) => (updater) => void.
api— объект с методамиgetState(),setState(),subscribe()next— вызов следующего middleware в цепочке (илиcoreSetStateесли последний)updater— переданный вsetStateаргумент
import type { Middleware } from "devlink-statecore";
// Middleware, который логирует время выполнения
const timing: Middleware<MyState> = (api) => (next) => (updater) => {
const start = performance.now();
next(updater);
const ms = (performance.now() - start).toFixed(2);
console.log(`setState: ${ms}ms`);
};
// Middleware, который блокирует обновления по условию
const guard: Middleware<MyState> = (api) => (next) => (updater) => {
const resolved =
typeof updater === "function" ? updater(api.getState()) : updater;
if (resolved.count !== undefined && resolved.count < 0) {
console.warn("Отрицательный count заблокирован");
return; // не вызываем next — обновление заблокировано
}
next(updater);
};
// Middleware, который трансформирует обновление
const clamp: Middleware<MyState> = () => (next) => (updater) => {
if (typeof updater === "object" && "count" in updater) {
next({ ...updater, count: Math.min(100, updater.count as number) });
} else {
next(updater);
}
};
const store = createStore({ count: 0 }, { middleware: [timing, guard, clamp] });Адаптеры для фреймворков
Адаптеры — отдельные entry points. Устанавливаются из того же пакета, но требуют соответствующий фреймворк в проекте.
React
Импорт: devlink-statecore/react
Требования: React 18+ (используется useSyncExternalStore).
useStore(store): Readonly<T>
Возвращает полное состояние. Компонент перерисовывается при любом изменении.
useStore(store, selector): R
Возвращает результат selector(state). Компонент перерисовывается только при изменении selected-значения.
import { useStore } from "devlink-statecore/react";
import { store } from "./store";
function Counter() {
// Только count — перерисовка только при изменении count
const count = useStore(store, (s) => s.count);
return (
<div>
<span data-testid="count">{count}</span>
<button
data-testid="increment"
onClick={() => store.setState((s) => ({ count: s.count + 1 }))}
>
+1
</button>
</div>
);
}
function UserInfo() {
// Полное состояние — перерисовка при любом изменении
const state = useStore(store);
return <span>{state.user}</span>;
}Vue 3
Импорт: devlink-statecore/vue
Требования: Vue 3+ (используется ref, readonly, onUnmounted).
useStore(store): DeepReadonly<Ref<T>>
Возвращает реактивный Ref с полным состоянием.
useStore(store, selector): Readonly<Ref<R>>
Возвращает реактивный Ref с результатом селектора.
<script setup lang="ts">
import { useStore } from "devlink-statecore/vue";
import { store } from "./store";
// Только count
const count = useStore(store, (s) => s.count);
// Полное состояние
const state = useStore(store);
function increment() {
store.setState((s) => ({ count: s.count + 1 }));
}
</script>
<template>
<div>
<span>{{ count }}</span>
<button @click="increment">+1</button>
</div>
</template>Svelte
Импорт: devlink-statecore/svelte
Svelte использует store contract: объект с методом subscribe(run) => unsubscribe. Адаптер toReadable создаёт совместимый объект.
toReadable(store): SvelteReadable<Readonly<T>>
Оборачивает стор в Svelte-readable. Доступен через $-синтаксис.
toReadable(store, selector): SvelteReadable<R>
Оборачивает стор с селектором.
<script>
import { toReadable } from "devlink-statecore/svelte";
import { store } from "./store";
const count = toReadable(store, (s) => s.count);
const state = toReadable(store);
function increment() {
store.setState((s) => ({ count: s.count + 1 }));
}
</script>
<span>{$count}</span>
<span>{$state.user}</span>
<button on:click={increment}>+1</button>Vanilla JS
Импорт: devlink-statecore/vanilla
Хелперы для работы с DOM и отслеживания срезов состояния без фреймворка.
bind(store, root, mapping): () => void
Привязывает состояние стора к DOM-элементам. При каждом обновлении находит элементы через querySelector и устанавливает textContent.
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| store | Store<T> | Стор |
| root | Element | Корневой DOM-элемент для поиска |
| mapping | Record<string, (state: T) => string> | Объект: CSS-селектор → функция, возвращающая текст |
Возвращает: () => void — функция отписки.
import { bind } from "devlink-statecore/vanilla";
const unsub = bind(store, document.getElementById("app")!, {
".count": (s) => String(s.count),
".user-name": (s) => s.user,
"[data-testid='status']": (s) => (s.count > 0 ? "active" : "idle"),
});
// Отписка от обновлений DOM
unsub();watchSelector(store, selector, callback): () => void
Следит за конкретным срезом состояния. Вызывает callback только когда значение selector(state) изменяется (сравнение через Object.is).
Параметры:
| Параметр | Тип | Описание |
| --- | --- | --- |
| store | Store<T> | Стор |
| selector | (state: T) => R | Функция-селектор, извлекающая нужный срез |
| callback | (value: R, prevValue: R) => void | Обработчик. Получает новое и предыдущее значение |
Возвращает: () => void — функция отписки.
import { watchSelector } from "devlink-statecore/vanilla";
// Следить за count
const unsub = watchSelector(
store,
(s) => s.count,
(next, prev) => {
console.log(`count: ${prev} → ${next}`);
},
);
// Следить за derived-значением
const unsub2 = watchSelector(
store,
(s) => s.count > 10,
(isAbove) => {
document.body.classList.toggle("highlight", isAbove);
},
);
unsub();
unsub2();Типы TypeScript
Все типы экспортируются из основного entry point:
import type {
Store,
StoreOptions,
Listener,
Selector,
Updater,
SetState,
GetState,
Subscribe,
Middleware,
MiddlewareAPI,
MiddlewareNext,
ComputedDef,
} from "devlink-statecore";| Тип | Описание |
| --- | --- |
| Store<T> | Интерфейс стора со всеми методами |
| StoreOptions<T> | Опции createStore: { name?, middleware? } |
| Listener<T> | (state: T, prevState: T) => void |
| Selector<T, R> | (state: T) => R |
| Updater<T> | Partial<T> \| ((state: T) => Partial<T>) |
| SetState<T> | (updater: Updater<T>) => void |
| GetState<T> | () => Readonly<T> |
| Subscribe<T> | (listener: Listener<T>) => () => void |
| Middleware<T> | (api: MiddlewareAPI<T>) => (next: MiddlewareNext<T>) => (updater: Updater<T>) => void |
| MiddlewareAPI<T> | { getState, setState, subscribe } — объект, доступный middleware |
| MiddlewareNext<T> | (updater: Updater<T>) => void — вызов следующего middleware |
| ComputedDef<T, R> | { deps: Selector<T, unknown>[], compute: (...args) => R } |
Типы middleware:
import type { LoggerOptions } from "devlink-statecore/middleware";
import type { DevToolsOptions } from "devlink-statecore/middleware";
import type { PersistOptions } from "devlink-statecore/middleware";Лицензия
MIT. Copyright (c) 2026 ООО «Айти Дев Линк».
Автор: Всеволод Белогрудов.
