Глобальный стейт без библиотек
Реактивное хранилище состояния на ванильном JS — паттерн Observer + singleton, без Redux/Zustand.
Задача
В небольшом приложении без фреймворка нужно общее состояние (авторизация, корзина, тема), изменения в котором автоматически обновляют UI в разных частях страницы.
Решение
// store.js
function createStore(initialState) {
let state = { ...initialState };
const listeners = new Set();
return {
getState {
return { ...state }; // иммутабельный снимок
},
setState(patch) {
state = { ...state, ...(typeof patch === 'function' ? patch(state) : patch) };
listeners.forEach((fn) => fn(state));
},
subscribe(fn) {
listeners.add(fn);
return => listeners.delete(fn); // отписка
},
};
}
// Синглтон
export const store = createStore({
user: null,
cart: ,
theme: 'light',
});
Использование:
import { store } from './store.js';
// Чтение
const { user } = store.getState;
// Изменение
store.setState({ theme: 'dark' });
// Функциональное обновление (для массивов/вложенных объектов)
store.setState((prev) => ({
cart: [...prev.cart, { id: 42, qty: 1 }],
}));
// Подписка на изменения
const unsubscribe = store.subscribe((state) => {
document.getElementById('cartCount').textContent = state.cart.length;
});
// Отписка
unsubscribe;
TypeScript-версия:
interface AppState {
user: { name: string } | null;
cart: Array<{ id: number; qty: number }>;
theme: 'light' | 'dark';
}
type Listener = (state: AppState) => void;
type Patch = Partial<AppState> | ((prev: AppState) => Partial<AppState>);
function createStore(init: AppState) {
let state = { ...init };
const listeners = new Set<Listener>;
return {
getState: : AppState => ({ ...state }),
setState(patch: Patch): void {
const next = typeof patch === 'function' ? patch(state) : patch;
state = { ...state, ...next() };
listeners.forEach((fn) => fn(state));
},
subscribe(fn: Listener): => void {
listeners.add(fn);
return => listeners.delete(fn);
},
};
}
Ключевые моменты
getStateвозвращает копию ({ ...state }) — предотвращает случайную мутацию снаружи.setStateпринимает как объект, так и функцию-updater (как в React) — для работы с предыдущим состоянием.Setвместо массива — исключает дублирование одного подписчика.- Подписка возвращает функцию отписки — удобно убирать слушателей при уничтожении компонента.
Когда НЕ использовать
- В React-приложениях — используй
Context + useReducer,ZustandилиJotai. - При сложных зависимостях между частями стейта — нужен полноценный state manager.