Глобальный стейт без библиотек

Реактивное хранилище состояния на ванильном 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.

Связанные рецепты / темы