@plasius/react-state
Version:
Tiny, testable, typesafe React Scoped Store helper.
191 lines (186 loc) • 5.26 kB
JavaScript
// 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