UNPKG

use-context-selector

Version:
252 lines (251 loc) 8.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useBridgeValue = exports.BridgeProvider = exports.useContextUpdate = exports.useContext = exports.useContextSelector = exports.createContext = void 0; const react_1 = require("react"); const scheduler_1 = require("scheduler"); const CONTEXT_VALUE = Symbol(); const ORIGINAL_PROVIDER = Symbol(); const isSSR = typeof window === 'undefined' || /ServerSideRendering/.test(window.navigator && window.navigator.userAgent); const useIsomorphicLayoutEffect = isSSR ? react_1.useEffect : react_1.useLayoutEffect; // for preact that doesn't have runWithPriority const runWithNormalPriority = scheduler_1.unstable_runWithPriority ? (fn) => { try { (0, scheduler_1.unstable_runWithPriority)(scheduler_1.unstable_NormalPriority, fn); } catch (e) { if (e.message === 'Not implemented.') { fn(); } else { throw e; } } } : (fn) => fn(); const createProvider = (ProviderOrig) => { const ContextProvider = ({ value, children, }) => { const valueRef = (0, react_1.useRef)(value); const versionRef = (0, react_1.useRef)(0); const [resolve, setResolve] = (0, react_1.useState)(null); if (resolve) { resolve(value); setResolve(null); } const contextValue = (0, react_1.useRef)(); if (!contextValue.current) { const listeners = new Set(); const update = (fn, options) => { versionRef.current += 1; const action = { n: versionRef.current, }; if (options === null || options === void 0 ? void 0 : options.suspense) { action.n *= -1; // this is intentional to make it temporary version action.p = new Promise((r) => { setResolve(() => (v) => { action.v = v; delete action.p; r(v); }); }); } listeners.forEach((listener) => listener(action)); fn(); }; contextValue.current = { [CONTEXT_VALUE]: { /* "v"alue */ v: valueRef, /* versio"n" */ n: versionRef, /* "l"isteners */ l: listeners, /* "u"pdate */ u: update, }, }; } useIsomorphicLayoutEffect(() => { valueRef.current = value; versionRef.current += 1; runWithNormalPriority(() => { contextValue.current[CONTEXT_VALUE].l.forEach((listener) => { listener({ n: versionRef.current, v: value }); }); }); }, [value]); return (0, react_1.createElement)(ProviderOrig, { value: contextValue.current }, children); }; return ContextProvider; }; const identity = (x) => x; /** * This creates a special context for `useContextSelector`. * * @example * import { createContext } from 'use-context-selector'; * * const PersonContext = createContext({ firstName: '', familyName: '' }); */ function createContext(defaultValue) { const context = (0, react_1.createContext)({ [CONTEXT_VALUE]: { /* "v"alue */ v: { current: defaultValue }, /* versio"n" */ n: { current: -1 }, /* "l"isteners */ l: new Set(), /* "u"pdate */ u: (f) => f(), }, }); context[ORIGINAL_PROVIDER] = context.Provider; context.Provider = createProvider(context.Provider); delete context.Consumer; // no support for Consumer return context; } exports.createContext = createContext; /** * This hook returns context selected value by selector. * * It will only accept context created by `createContext`. * It will trigger re-render if only the selected value is referentially changed. * * The selector should return referentially equal result for same input for better performance. * * @example * import { useContextSelector } from 'use-context-selector'; * * const firstName = useContextSelector(PersonContext, (state) => state.firstName); */ function useContextSelector(context, selector) { const contextValue = (0, react_1.useContext)(context)[CONTEXT_VALUE]; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { if (!contextValue) { throw new Error('useContextSelector requires special context'); } } const { /* "v"alue */ v: { current: value }, /* versio"n" */ n: { current: version }, /* "l"isteners */ l: listeners, } = contextValue; const selected = selector(value); const [state, dispatch] = (0, react_1.useReducer)((prev, action) => { if (!action) { // case for `dispatch()` below return [value, selected]; } if ('p' in action) { throw action.p; } if (action.n === version) { if (Object.is(prev[1], selected)) { return prev; // bail out } return [value, selected]; } try { if ('v' in action) { if (Object.is(prev[0], action.v)) { return prev; // do not update } const nextSelected = selector(action.v); if (Object.is(prev[1], nextSelected)) { return prev; // do not update } return [action.v, nextSelected]; } } catch (_e) { // ignored (stale props or some other reason) } return [...prev]; // schedule update }, [value, selected]); if (!Object.is(state[1], selected)) { // schedule re-render // this is safe because it's self contained dispatch(); } useIsomorphicLayoutEffect(() => { listeners.add(dispatch); return () => { listeners.delete(dispatch); }; }, [listeners]); return state[1]; } exports.useContextSelector = useContextSelector; /** * This hook returns the entire context value. * Use this instead of React.useContext for consistent behavior. * * @example * import { useContext } from 'use-context-selector'; * * const person = useContext(PersonContext); */ function useContext(context) { return useContextSelector(context, identity); } exports.useContext = useContext; /** * This hook returns an update function to wrap an updating function * * Use this for a function that will change a value in * concurrent rendering in React 18. * Otherwise, there's no need to use this hook. * * @example * import { useContextUpdate } from 'use-context-selector'; * * const update = useContextUpdate(); * * // Wrap set state function * update(() => setState(...)); * * // Experimental suspense mode * update(() => setState(...), { suspense: true }); */ function useContextUpdate(context) { const contextValue = (0, react_1.useContext)(context)[CONTEXT_VALUE]; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { if (!contextValue) { throw new Error('useContextUpdate requires special context'); } } const { u: update } = contextValue; return update; } exports.useContextUpdate = useContextUpdate; /* eslint-disable @typescript-eslint/no-explicit-any */ /** * This is a Provider component for bridging multiple react roots * * @example * const valueToBridge = useBridgeValue(PersonContext); * return ( * <Renderer> * <BridgeProvider context={PersonContext} value={valueToBridge}> * {children} * </BridgeProvider> * </Renderer> * ); */ const BridgeProvider = ({ context, value, children, }) => { const { [ORIGINAL_PROVIDER]: ProviderOrig } = context; if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { if (!ProviderOrig) { throw new Error('BridgeProvider requires special context'); } } return (0, react_1.createElement)(ProviderOrig, { value }, children); }; exports.BridgeProvider = BridgeProvider; /** * This hook return a value for BridgeProvider */ const useBridgeValue = (context) => { const bridgeValue = (0, react_1.useContext)(context); if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { if (!bridgeValue[CONTEXT_VALUE]) { throw new Error('useBridgeValue requires special context'); } } return bridgeValue; }; exports.useBridgeValue = useBridgeValue;