UNPKG

react-tately

Version:

Minimal, powerful global state manager for React

73 lines (61 loc) 2.02 kB
import { useSyncExternalStore } from 'react'; type Listener = () => void; type Updater<T> = (prev: T) => T; type Middleware<T> = (prev: T, next: T) => void; interface StoreOptions<T> { name?: string; persist?: boolean; devtools?: boolean; middlewares?: Middleware<T>[]; } function getKey(name?: string) { return `__stately__${name ?? 'default'}`; } function loadPersistedState<T>(defaultState: T, options?: StoreOptions<T>): T { if (typeof window === 'undefined' || !options?.persist) return defaultState; const raw = localStorage.getItem(getKey(options.name)); if (!raw) return defaultState; try { return JSON.parse(raw); } catch { return defaultState; } } export function createStore<T>( initialValue: T, options?: StoreOptions<T> ): () => [T, (updater: Updater<T>) => void] { let state: T = loadPersistedState(initialValue, options); const listeners = new Set<Listener>(); const middlewares = options?.middlewares ?? []; const notify = () => { listeners.forEach((listener) => listener()); if (options?.devtools && typeof window !== 'undefined') { window.__STATELY_DEVTOOLS__?.({ name: options.name, state }); } }; const setState = (updater: Updater<T>) => { const nextState = updater(state); middlewares.forEach((mw) => mw(state, nextState)); state = nextState; if (options?.persist) { localStorage.setItem(getKey(options.name), JSON.stringify(state)); } notify(); }; const subscribe = (listener: Listener) => { listeners.add(listener); return () => listeners.delete(listener); }; const useStore = (): [T, (updater: Updater<T>) => void] => { const snapshot = useSyncExternalStore(subscribe, () => state); return [snapshot, setState]; }; return useStore; } // Global devtools extension hook declare global { interface Window { __STATELY_DEVTOOLS__?: (payload: { name?: string; state: unknown }) => void; } }