UNPKG

@dr.pogodin/react-global-state

Version:
147 lines (137 loc) 6.17 kB
// Hook for updates of global state. import { isFunction } from 'lodash'; import { useEffect, useRef, useSyncExternalStore } from 'react'; import { Emitter } from '@dr.pogodin/js-utils'; import { getGlobalState } from "./GlobalStateProvider"; import { cloneDeepForLog, isDebugMode } from "./utils"; /** * 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 ) { const globalState = getGlobalState(); const ref = useRef(undefined); // TODO: Revise how this `rc` variable is used, perhaps we can simplify stuff // here. let rc = ref.current; if (!ref.current) { const emitter = new Emitter(); ref.current = { emitter, globalState, path, setter: value => { const newState = isFunction(value) ? value(rc.globalState.get(rc.path)) : value; if (process.env.NODE_ENV !== 'production' && isDebugMode()) { var _path, _path2; /* eslint-disable no-console */ console.groupCollapsed(`ReactGlobalState - useGlobalState setter triggered for path ${(_path = rc.path) !== null && _path !== void 0 ? _path : ''}`); console.log('New value:', cloneDeepForLog(newState, (_path2 = rc.path) !== null && _path2 !== void 0 ? _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.state) rc.emitter.emit(); }, state: isFunction(initialValue) ? initialValue() : initialValue, subscribe: emitter.addListener.bind(emitter), watcher: () => { // TODO: Revise it later. // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression const state = rc.globalState.get(rc.path); if (state !== rc.state) rc.emitter.emit(); } }; } rc = ref.current; rc.globalState = globalState; rc.path = path; rc.state = useSyncExternalStore(rc.subscribe, () => rc.globalState.get(rc.path, { initialValue }), () => rc.globalState.get(rc.path, { initialState: true, initialValue })); useEffect(() => { const { watcher } = ref.current; globalState.watch(watcher); watcher(); return () => { globalState.unWatch(watcher); }; }, [globalState]); useEffect(() => { ref.current.watcher(); }, [path]); return [rc.state, rc.setter]; } export default useGlobalState; // TODO: Revise. // eslint-disable-next-line @typescript-eslint/consistent-type-definitions //# sourceMappingURL=useGlobalState.js.map