@dr.pogodin/react-global-state
Version:
Hook-based global state for React
149 lines (140 loc) • 6.24 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _lodash = require("lodash");
var _react = require("react");
var _jsUtils = require("@dr.pogodin/js-utils");
var _GlobalStateProvider = require("./GlobalStateProvider");
var _utils = require("./utils");
// Hook for updates of global state.
/**
* 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 = (0, _GlobalStateProvider.getGlobalState)();
const ref = (0, _react.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 _jsUtils.Emitter();
ref.current = {
emitter,
globalState,
path,
setter: value => {
const newState = (0, _lodash.isFunction)(value) ? value(rc.globalState.get(rc.path)) : value;
if (process.env.NODE_ENV !== 'production' && (0, _utils.isDebugMode)()) {
/* eslint-disable no-console */
console.groupCollapsed(`ReactGlobalState - useGlobalState setter triggered for path ${rc.path ?? ''}`);
console.log('New value:', (0, _utils.cloneDeepForLog)(newState, rc.path ?? ''));
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: (0, _lodash.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 = (0, _react.useSyncExternalStore)(rc.subscribe, () => rc.globalState.get(rc.path, {
initialValue
}), () => rc.globalState.get(rc.path, {
initialState: true,
initialValue
}));
(0, _react.useEffect)(() => {
const {
watcher
} = ref.current;
globalState.watch(watcher);
watcher();
return () => {
globalState.unWatch(watcher);
};
}, [globalState]);
(0, _react.useEffect)(() => {
ref.current.watcher();
}, [path]);
return [rc.state, rc.setter];
}
var _default = exports.default = useGlobalState; // TODO: Revise.
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
//# sourceMappingURL=useGlobalState.js.map