UNPKG

@dr.pogodin/react-global-state

Version:
141 lines (137 loc) 6.11 kB
// Hook for updates of global state. import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import { Emitter } from '@dr.pogodin/js-utils'; import { getGlobalState } from "./GlobalStateProvider.js"; import { cloneDeepForLog, isDebugMode } from "./utils.js"; /** * The primary hook for interacting with the global state, modeled after * the standard React's * [useState](https://reactjs.org/docs/hooks-reference.html#usestate). * It subscribes a component to a given `path` of global state, and provides * a function to update it. Each time the value at `path` changes, the hook * triggers re-render of its host component. * * **Note:** * - For performance, the library does not copy objects written to / read from * global state paths. You MUST NOT manually mutate returned state values, * or change objects already written into the global state, without explicitly * clonning them first yourself. * - State update notifications are asynchronous. When your code does multiple * global state updates in the same React rendering cycle, all state update * notifications are queued and dispatched together, after the current * rendering cycle. In other words, in any given rendering cycle the global * state values are "fixed", and all changes becomes visible at once in the * next triggered rendering pass. * * @param path Dot-delimitered state path. It can be undefined to * subscribe for entire state. * * Under-the-hood state values are read and written using `lodash` * [_.get()](https://lodash.com/docs/4.17.15#get) and * [_.set()](https://lodash.com/docs/4.17.15#set) methods, thus it is safe * to access state paths which have not been created before. * @param initialValue Initial value to set at the `path`, or its * factory: * - If a function is given, it will act similar to * [the lazy initial state of the standard React's useState()](https://reactjs.org/docs/hooks-reference.html#lazy-initial-state): * only if the value at `path` is `undefined`, the function will be executed, * and the value it returns will be written to the `path`. * - Otherwise, the given value itself will be written to the `path`, * if the current value at `path` is `undefined`. * @return It returs an array with two elements: `[value, setValue]`: * * - The `value` is the current value at given `path`. * * - The `setValue()` is setter function to write a new value to the `path`. * * Similar to the standard React's `useState()`, it supports * [functional value updates](https://reactjs.org/docs/hooks-reference.html#functional-updates): * if `setValue()` is called with a function as argument, that function will * be called and its return value will be written to `path`. Otherwise, * the argument of `setValue()` itself is written to `path`. * * Also, similar to the standard React's state setters, `setValue()` is * stable function: it does not change between component re-renders. */ // "Enforced type overload" // "Entire state overload" // "State evaluation overload" function useGlobalState(path, // TODO: Revise it later! // eslint-disable-next-line @typescript-eslint/no-explicit-any initialValue // TODO: Revise it later! // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { var _ref$current; const globalState = getGlobalState(); const ref = useRef(null); const [stable] = useState(() => { const emitter = new Emitter(); const setter = value => { const rc = ref.current; if (!rc) throw Error('Internal error'); const newState = typeof value === 'function' ? value(rc.globalState.get(rc.path)) : value; if (process.env.NODE_ENV !== 'production' && isDebugMode()) { var _rc$path, _rc$path2; /* eslint-disable no-console */ console.groupCollapsed(`ReactGlobalState - useGlobalState setter triggered for path ${(_rc$path = rc.path) !== null && _rc$path !== void 0 ? _rc$path : ''}`); console.log('New value:', cloneDeepForLog(newState, (_rc$path2 = rc.path) !== null && _rc$path2 !== void 0 ? _rc$path2 : '')); console.groupEnd(); /* eslint-enable no-console */ } rc.globalState.set(rc.path, newState); // NOTE: The regular global state's update notifications, automatically // triggered by the rc.globalState.set() call above, are batched, and // scheduled to fire asynchronosuly at a later time, which is problematic // for managed text inputs - if they have their value update delayed to // future render cycles, it will result in reset of their cursor position // to the value end. Calling the rc.emitter.emit() below causes a sooner // state update for the current component, thus working around the issue. // For additional details see the original issue: // https://github.com/birdofpreyru/react-global-state/issues/22 if (newState !== rc.prevValue) emitter.emit(); }; const subscribe = emitter.addListener.bind(emitter); return { emitter, setter, subscribe }; }); const value = useSyncExternalStore(stable.subscribe, () => globalState.get(path, { initialValue }), () => globalState.get(path, { initialState: true, initialValue })); (_ref$current = ref.current) !== null && _ref$current !== void 0 ? _ref$current : ref.current = { globalState, path, prevValue: value }; useEffect(() => { ref.current = { globalState, path, prevValue: ref.current.prevValue }; const watcher = () => { const nextValue = globalState.get(path); if (ref.current.prevValue !== nextValue) { ref.current.prevValue = nextValue; stable.emitter.emit(); } }; globalState.watch(watcher); watcher(); return () => { globalState.unWatch(watcher); }; }, [globalState, stable.emitter, path]); return [value, stable.setter]; } export default useGlobalState; // TODO: Revise. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions //# sourceMappingURL=useGlobalState.js.map