react-tately
Version:
Minimal, powerful global state manager for React
73 lines (61 loc) • 2.02 kB
text/typescript
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;
}
}