UNPKG

redext

Version:

A simple global store based on React Context and Hooks

487 lines (463 loc) 14.2 kB
// src/Provider.tsx import { useReducer, useRef } from "react"; // src/Context.tsx import { createContext } from "react"; var Context_default = createContext({}); // src/Provider.tsx import { jsx } from "react/jsx-runtime"; var Provider = (props) => { const { store, initialValue, ...providerProps } = props; if (!store) { throw new Error("Please use <Provider store={...} initialValue={...}>"); } const listeners = /* @__PURE__ */ new Set(); const initialState = store.getState(initialValue); const [state, dispatch] = useReducer(store.getReducer, initialState); const stateRef = useRef(initialState); stateRef.current = state; const getState = () => { return stateRef.current; }; const { effects, dispatch: dispatcher, models, on } = store.getEffect(dispatch, state); const subscribe = (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; }; const value = { subscribe, getState, state: getState(), dispatch: (arg) => { on("onModel", (onModel) => { var _a; const type = arg == null ? void 0 : arg.type; const types = (_a = type == null ? void 0 : type.split) == null ? void 0 : _a.call(type, "/"); const modelName = types == null ? void 0 : types[0]; if (!modelName) { return; } const actionName = types == null ? void 0 : types[1]; const model = models[modelName]; onModel({ model: { ...model, name: modelName }, modelName, actionName, dispatch: dispatcher }); }); dispatcher(arg); listeners.forEach((listener) => listener()); }, effects }; return /* @__PURE__ */ jsx( Context_default.Provider, { value, ...providerProps } ); }; var Provider_default = Provider; // src/createStore.ts var merge = (original, extra) => { return extra ? { ...extra, ...original } : original; }; function createStore(config) { const { plugins = [] } = config; let models = config.models; plugins.forEach((plugin) => { if (plugin.config) { models = merge(models, plugin.config.models); } }); const on = (eventName, callback) => { plugins.forEach((plugin) => { if (plugin[eventName]) { callback(plugin[eventName]); } }); }; const getState = (initialState = {}) => { const newInitialState = {}; Object.keys(models).forEach((modelFilename) => { const model = models[modelFilename] || {}; const modelName = model.name || modelFilename; newInitialState[modelName] = Object.assign({}, model.state, initialState[modelName]); }); return newInitialState; }; const getReducer = (state = {}, action = {}) => { const newState = {}; Object.keys(models).forEach((modelFilename) => { var _a, _b; const model = models[modelFilename] || {}; const { reducers = {}, name: modelName = modelFilename } = model; let reducerState = state[modelName]; const actionType = (_b = (_a = action.type) == null ? void 0 : _a.replace) == null ? void 0 : _b.call(_a, `${modelName}/`, ""); if (actionType && actionType in reducers) { reducerState = reducers[actionType](reducerState, action.payload, action.params); } newState[modelName] = reducerState; }); return newState; }; const getEffect = (dispatch, state = {}) => { const newEffects = {}; Object.keys(models).forEach((modelFilename) => { const modelDispatcher = {}; const model = models[modelFilename] || {}; const { reducers = {}, effects: effectsFromConfig, name: modelName = modelFilename } = model; modelDispatcher.state = state[modelName]; const onModelListener = ({ actionName }) => { on("onModel", (onModel) => { onModel({ model: { ...model, name: modelName }, modelName, actionName, dispatch }); }); }; Object.keys(reducers).forEach((actionName) => { const type = `${modelName}/${actionName}`; modelDispatcher[actionName] = (payload, params) => { dispatch({ type, payload, params }); }; }); let effects; dispatch[modelName] = modelDispatcher; if (typeof effectsFromConfig === "function") { effects = effectsFromConfig(dispatch); } else { effects = effectsFromConfig || {}; } const effectObj = {}; Object.keys(effects).forEach((effectName) => { const effectFunc = (...args) => { onModelListener({ actionName: effectName }); return effects[effectName].apply(modelDispatcher, args); }; modelDispatcher[effectName] = effectFunc; effectObj[effectName] = effectFunc; }); newEffects[modelName] = effectObj; }); return { effects: newEffects, models, dispatch, on }; }; return { getState, getReducer, getEffect }; } // src/connect.tsx import { useContext as useContext3 } from "react"; // src/hooks/useContextSelector.ts import { useContext, useSyncExternalStore } from "react"; // src/hooks/useDeepMemo.ts import React2 from "react"; // src/hooks/useDeepMemoize.ts import { useRef as useRef2 } from "react"; // src/utils/deepEqual.ts var equal = (a, b) => { let ctor; let len; if (a === b) { return true; } if (a && b && (ctor = a.constructor) === b.constructor) { if (ctor === Date) { return a.getTime() === b.getTime(); } if (ctor === RegExp) { return a.toString() === b.toString(); } if (ctor === Array && (len = a.length) === b.length) { while (len-- && equal(a[len], b[len])) ; return len === -1; } if (ctor === Object) { if (Object.keys(a).length !== Object.keys(b).length) { return false; } for (len in a) { if (!(len in b) || !equal(a[len], b[len])) { return false; } } return true; } } return a !== a && b !== b; }; function deepEqual(a, b) { try { return equal(a, b); } catch (error) { if (error.message && error.message.match(/stack|recursion/i) || error.number === -2146828260) { console.warn("Warning: deepEqual does not handle circular references.", error.name, error.message); return false; } throw error; } } // src/hooks/useDeepMemoize.ts var useDeepMemoize = (value) => { const ref = useRef2([]); if (!deepEqual(value, ref.current)) { ref.current = value; } return ref.current; }; var useDeepMemoize_default = useDeepMemoize; // src/utils/checkDeps.ts var isPrimitive = (val) => { return val == null || /^[sbn]/.test(typeof val); }; var checkDeps = (deps, name) => { const hookName = `React.${name.replace(/Deep/, "")}`; if (!deps || deps.length === 0) { console.warn(`${name} should not be used with no dependencies. Use ${hookName} instead.`); } if (deps.every(isPrimitive)) { console.warn(`${name} should not be used with dependencies that are all primitive values. Use ${hookName} instead.`); } }; var checkDeps_default = checkDeps; // src/hooks/useDeepMemo.ts var useDeepMemo = (factory, dependencies) => { if (process.env.NODE_ENV !== "production") { checkDeps_default(dependencies, "useDeepMemo"); } return React2.useMemo(factory, useDeepMemoize_default(dependencies)); }; var useDeepMemo_default = useDeepMemo; // src/hooks/useContextSelector.ts var useContextSelector = (mapStateToProps, params = {}) => { const { isWithSyncExternalStore = true } = params; if (isWithSyncExternalStore && useSyncExternalStore) { const { subscribe, getState } = useContext(Context_default); const getSnapshot = () => { if (!mapStateToProps) { return void 0; } const state2 = getState(); return mapStateToProps(state2); }; return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); } const { state = {} } = useContext(Context_default); let filteredState = {}; if (mapStateToProps) { filteredState = mapStateToProps(state); } else { filteredState = state; } return useDeepMemo_default(() => { return filteredState; }, [filteredState]); }; var useContextSelector_default = useContextSelector; // src/hooks/useDispatcher.ts import { useContext as useContext2 } from "react"; var useDispatcher = (mapDispatchToProps) => { const { dispatch, effects } = useContext2(Context_default); let filteredDispatch = {}; if (mapDispatchToProps) { filteredDispatch = mapDispatchToProps(effects, dispatch); } return filteredDispatch; }; var useDispatcher_default = useDispatcher; // src/connect.tsx import { jsx as jsx2 } from "react/jsx-runtime"; var connect = (mapStateToProps, mapDispatchToProps) => (Component) => { return (props) => { const { dispatch } = useContext3(Context_default); const memoState = useContextSelector_default(mapStateToProps, { isWithSyncExternalStore: false }); const dispatcher = useDispatcher_default(mapDispatchToProps); return /* @__PURE__ */ jsx2( Component, { ...props, ...memoState, ...dispatcher, dispatch } ); }; }; var connect_default = connect; // src/memoize.ts import React4 from "react"; // src/utils/isEqual.ts var isArray = Array.isArray; var keyList = Object.keys; var hasProp = Object.prototype.hasOwnProperty; var hasElementType = typeof Element !== "undefined"; function equal2(a, b) { if (a === b) { return true; } if (a && b && typeof a == "object" && typeof b == "object") { const arrA = isArray(a); const arrB = isArray(b); let length; let key; if (arrA && arrB) { length = a.length; if (length !== b.length) { return false; } for (let i = 0; i < length; i++) { if (!equal2(a[i], b[i])) { return false; } } return true; } if (arrA !== arrB) { return false; } const dateA = a instanceof Date; const dateB = b instanceof Date; if (dateA !== dateB) { return false; } if (dateA && dateB) { return a.getTime() === b.getTime(); } const regexpA = a instanceof RegExp; const regexpB = b instanceof RegExp; if (regexpA !== regexpB) { return false; } if (regexpA && regexpB) { return a.toString() === b.toString(); } const keys = keyList(a); length = keys.length; if (length !== keyList(b).length) return false; for (let i = 0; i < length; i++) { if (!hasProp.call(b, keys[i])) { return false; } } if (hasElementType && a instanceof Element) { return false; } for (let i = 0; i < length; i++) { key = keys[i]; if (key === "_owner" && a.$$typeof) { continue; } else { if (!equal2(a[key], b[key])) { return false; } } } return true; } return a !== a && b !== b; } function exportedEqual(a, b) { try { return equal2(a, b); } catch (error) { if (error.message && error.message.match(/stack|recursion/i) || error.number === -2146828260) { console.warn("Warning: isEqual does not handle circular references.", error.name, error.message); return false; } throw error; } } // src/memoize.ts var shallowDiffers = (prev, next) => { for (let attribute in prev) { if (!(attribute in next)) { return true; } } for (let attribute in next) { if (!exportedEqual(prev[attribute], next[attribute])) { return true; } } return false; }; var areEqual = (prevProps, nextProps) => { const { style: prevStyle, ...prevRest } = prevProps; const { style: nextStyle, ...nextRest } = nextProps; return !shallowDiffers(prevStyle, nextStyle) && !shallowDiffers(prevRest, nextRest); }; var memoize = (Component, memoPropsAreEqual = areEqual) => { return React4.memo(Component, memoPropsAreEqual); }; var memoize_default = memoize; // src/hooks/useDispatch.ts import { useContext as useContext4 } from "react"; function useDispatch() { const { dispatch } = useContext4(Context_default); return dispatch; } // src/hooks/useSelector.ts var useSelector = (mapStateToProps) => { return useContextSelector_default(mapStateToProps); }; var useSelector_default = useSelector; // src/hooks/useDeepEffect.ts import React5 from "react"; var useDeepEffect = (effect, dependencies) => { if (process.env.NODE_ENV !== "production") { checkDeps_default(dependencies, "useDeepEffect"); } return React5.useEffect(effect, useDeepMemoize_default(dependencies)); }; var useDeepEffect_default = useDeepEffect; // src/hooks/useDeepIsomorphicLayoutEffect.ts import { useEffect, useLayoutEffect } from "react"; var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; var useDeepIsomorphicLayoutEffect = (effect, dependencies) => { if (process.env.NODE_ENV !== "production") { checkDeps_default(dependencies, "useDeepEffect"); } return useIsomorphicLayoutEffect(effect, useDeepMemoize_default(dependencies)); }; var useDeepIsomorphicLayoutEffect_default = useDeepIsomorphicLayoutEffect; // src/hooks/useDeepCallback.ts import React6 from "react"; var useDeepCallback = (callback, dependencies) => { if (process.env.NODE_ENV !== "production") { checkDeps_default(dependencies, "useDeepCallback"); } return React6.useCallback(callback, useDeepMemoize_default(dependencies)); }; var useDeepCallback_default = useDeepCallback; // src/index.ts var init = (config) => { return createStore(config); }; export { Provider_default as Provider, connect_default as connect, deepEqual, init, exportedEqual as isEqual, memoize_default as memoize, useDeepCallback_default as useDeepCallback, useDeepEffect_default as useDeepEffect, useDeepIsomorphicLayoutEffect_default as useDeepIsomorphicLayoutEffect, useDeepMemo_default as useDeepMemo, useDispatch, useDispatcher_default as useDispatcher, useSelector_default as useSelector };