UNPKG

@plasius/react-state

Version:

Tiny, testable, typesafe React Scoped Store helper.

191 lines (186 loc) 5.26 kB
// src/types.ts var __noop = null; // src/create-scoped-store.tsx import { createContext, useContext, useRef, useSyncExternalStore } from "react"; // src/store.ts function createStore(reducer, initialState) { let state = initialState; const listeners = /* @__PURE__ */ new Set(); const keyListeners = /* @__PURE__ */ new Map(); const selectorListeners = /* @__PURE__ */ new Set(); const getState = () => state; const dispatch = (action) => { const prevState = state; const nextState = reducer(state, action); if (Object.is(prevState, nextState)) { state = nextState; return; } state = nextState; for (const listener of [...listeners]) listener(); for (const [key, set] of keyListeners.entries()) { if (!Object.is(prevState[key], state[key])) { for (const listener of [...set]) listener(state[key]); } } selectorListeners.forEach((entry) => { const nextValue = entry.selector(state); if (!Object.is(entry.lastValue, nextValue)) { entry.lastValue = nextValue; entry.listener(nextValue); } }); }; const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; const subscribeToKey = (key, listener) => { const set = keyListeners.get(key) ?? /* @__PURE__ */ new Set(); set.add(listener); keyListeners.set(key, set); return () => { set.delete(listener); if (set.size === 0) keyListeners.delete(key); }; }; const subscribeWithSelector = (selector, listener) => { const entry = { selector, listener, lastValue: selector(state) }; selectorListeners.add(entry); return () => { selectorListeners.delete(entry); }; }; return { getState, dispatch, subscribe, subscribeToKey, subscribeWithSelector }; } // src/create-scoped-store.tsx import { jsx } from "react/jsx-runtime"; function shallowEqual(a, b) { if (Object.is(a, b)) return true; if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false; const ak = Object.keys(a), bk = Object.keys(b); if (ak.length !== bk.length) return false; for (let i = 0; i < ak.length; i++) { const k = ak[i]; if (!Object.prototype.hasOwnProperty.call(b, k) || !Object.is(a[k], b[k])) return false; } return true; } function createScopedStoreContext(reducer, initialState) { const Context = createContext(null); const store = createStore(reducer, initialState); const Provider = ({ children }) => /* @__PURE__ */ jsx(Context.Provider, { value: store, children }); const useStore2 = () => { const ctx = useContext(Context); if (!ctx) throw new Error("Store not found in context"); return useSyncExternalStore(ctx.subscribe, ctx.getState, ctx.getState); }; const useDispatch2 = () => { const ctx = useContext(Context); if (!ctx) throw new Error("Dispatch not found in context"); return (action) => ctx.dispatch(action); }; function useSelector(selector, isEqual = shallowEqual) { const ctx = useContext(Context); if (!ctx) throw new Error("Store not found in context"); const state = useSyncExternalStore( ctx.subscribe, ctx.getState, ctx.getState ); const lastRef = useRef(null); const last = lastRef.current; const nextSelected = selector(state); if (last && last.state === state && isEqual(last.selected, nextSelected)) { return last.selected; } lastRef.current = { state, selected: nextSelected }; return nextSelected; } return { store, Context, Provider, useStore: useStore2, useDispatch: useDispatch2, useSelector }; } // src/provider.tsx import { createContext as createContext2, useContext as useContext2, useEffect, useState } from "react"; import { jsx as jsx2 } from "react/jsx-runtime"; var StoreContext = createContext2(void 0); function useStoreInstance() { const store = useContext2(StoreContext); if (!store) { throw new Error( "StoreProvider is missing in the React tree. Wrap your app with <StoreProvider store={...}>." ); } return store; } function StoreProvider({ store, children }) { return /* @__PURE__ */ jsx2(StoreContext.Provider, { value: store, children }); } function useStore() { const store = useStoreInstance(); const [state, setState] = useState(() => store.getState()); useEffect(() => { const unsubscribe = store.subscribe(() => { setState(store.getState()); }); return unsubscribe; }, [store]); return state; } function useDispatch() { const store = useStoreInstance(); return store.dispatch; } // src/metadata-store.ts var MetadataStore = class { symbol; constructor(description) { this.symbol = Symbol(description); } set(target, meta) { Object.defineProperty(target, this.symbol, { value: meta, writable: false, enumerable: false }); } get(target) { return target[this.symbol]; } has(target) { return this.symbol in target; } }; export { MetadataStore, StoreProvider, __noop, createScopedStoreContext, createStore, useDispatch, useStore }; //# sourceMappingURL=index.js.map