UNPKG

overmind-react

Version:
259 lines • 8.88 kB
import * as React from 'react'; import { ENVIRONMENT, EventType, MODE_SSR, } from 'overmind'; const IS_PRODUCTION = ENVIRONMENT === 'production'; const IS_TEST = ENVIRONMENT === 'test'; const isNode = !IS_TEST && typeof process !== 'undefined' && process.title && process.title.includes('node'); function getFiberType(component) { if (component.type) { // React.memo return getFiberType(component.type); } // React.forwardRef return component.render || component; } // Inspired from https://github.com/facebook/react/blob/master/packages/react-devtools-shared/src/backend/renderer.js function getDisplayName(component) { const type = getFiberType(component); return type.displayName || type.name || 'Anonymous'; } function throwMissingContextError() { throw new Error('The Overmind hook could not find an Overmind instance on the context of React. Please make sure you use the Provider component at the top of your application and expose the Overmind instance there. Please read more in the React guide on the website'); } const context = React.createContext({}); let nextComponentId = 0; export const Provider = context.Provider; function useForceRerender() { const [flushId, forceRerender] = React.useState(-1); return { flushId, forceRerender, }; } let currentComponentInstanceId = 0; const { ReactCurrentOwner } = React .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; const useCurrentComponent = () => { return ReactCurrentOwner && ReactCurrentOwner.current && ReactCurrentOwner.current.elementType ? ReactCurrentOwner.current.elementType : {}; }; class ReactTrackerV18 { constructor(tree) { this.subscribe = (cb) => { this.cb = cb; return () => { if (IS_PRODUCTION) { this.tree.dispose(); } else { // In development we do not dispose of the tree as React will do // "test runs" on effect hooks, it will rather be disposed when there is an // update to the tracked paths, but there is no longer a callback to trigger. This // can cause memory leaks in edge cases, but this is just development and does not matter delete this.cb; } }; }; this.getState = () => { return this.result; }; this.tree = tree; this.result = { state: tree.state }; this.updateCb = () => { this.result = { state: this.tree.state, }; if (this.cb) { this.cb(); } else { this.tree.dispose(); } }; } track() { this.tree.track(this.updateCb); } stopTracking() { this.tree.stopTracking(); } } const useStateV18 = (cb) => { const overmind = React.useContext(context); if (!overmind.mode) { throwMissingContextError(); } if (isNode || overmind.mode.mode === MODE_SSR) { return overmind.state; } const ref = React.useRef(null); if (!ref.current) { // @ts-ignore ref.current = new ReactTrackerV18(overmind.getTrackStateTree()); } const tracker = ref.current; const snapshot = React.useSyncExternalStore(tracker.subscribe, tracker.getState, tracker.getState); const mountedRef = React.useRef(false); // @ts-ignore const state = cb ? cb(snapshot.state) : snapshot.state; tracker.track(); if (IS_PRODUCTION) { React.useLayoutEffect(() => { tracker.stopTracking(); }, [tracker]); } else { const component = useCurrentComponent(); const name = getDisplayName(component); component.__componentId = typeof component.__componentId === 'undefined' ? nextComponentId++ : component.__componentId; const { current: componentInstanceId } = React.useRef(currentComponentInstanceId++); React.useLayoutEffect(() => { mountedRef.current = true; overmind.eventHub.emitAsync(EventType.COMPONENT_ADD, { componentId: component.__componentId, componentInstanceId, name, paths: Array.from(tracker.tree.pathDependencies), }); return () => { mountedRef.current = false; overmind.eventHub.emitAsync(EventType.COMPONENT_REMOVE, { componentId: component.__componentId, componentInstanceId, name, }); }; }, []); React.useLayoutEffect(() => { tracker.stopTracking(); overmind.eventHub.emitAsync(EventType.COMPONENT_UPDATE, { componentId: component.__componentId, componentInstanceId, name, flushId: 0, paths: Array.from(tracker.tree.pathDependencies), }); }, [tracker]); } return state; }; const useState = (cb) => { const overmind = React.useContext(context); if (!overmind.mode) { throwMissingContextError(); } if (isNode || overmind.mode.mode === MODE_SSR) { return overmind.state; } const mountedRef = React.useRef(false); const { flushId, forceRerender } = useForceRerender(); const tree = React.useMemo(() => overmind.proxyStateTreeInstance.getTrackStateTree(), [flushId]); const state = cb ? cb(tree.state) : tree.state; if (IS_PRODUCTION) { React.useLayoutEffect(() => { mountedRef.current = true; tree.stopTracking(); return () => { tree.dispose(); }; }, [tree]); tree.track((_, __, flushId) => { if (!mountedRef.current) { // This one is not dealt with by the useLayoutEffect tree.dispose(); return; } forceRerender(flushId); }); } else { const component = useCurrentComponent(); const name = getDisplayName(component); component.__componentId = typeof component.__componentId === 'undefined' ? nextComponentId++ : component.__componentId; const { current: componentInstanceId } = React.useRef(currentComponentInstanceId++); React.useLayoutEffect(() => { mountedRef.current = true; overmind.eventHub.emitAsync(EventType.COMPONENT_ADD, { componentId: component.__componentId, componentInstanceId, name, paths: Array.from(tree.pathDependencies), }); return () => { mountedRef.current = false; overmind.eventHub.emitAsync(EventType.COMPONENT_REMOVE, { componentId: component.__componentId, componentInstanceId, name, }); }; }, []); React.useLayoutEffect(() => { tree.stopTracking(); overmind.eventHub.emitAsync(EventType.COMPONENT_UPDATE, { componentId: component.__componentId, componentInstanceId, name, flushId, paths: Array.from(tree.pathDependencies), }); return () => { tree.dispose(); }; }, [tree]); tree.track((_, __, flushId) => { if (!mountedRef.current) { // This one is not dealt with by the useLayoutEffect tree.dispose(); return; } forceRerender(flushId); }); } return state; }; const useActions = () => { const overmind = React.useContext(context); if (!overmind.mode) { throwMissingContextError(); } return overmind.actions; }; const useEffects = () => { const overmind = React.useContext(context); if (!overmind.mode) { throwMissingContextError(); } return overmind.effects; }; const useReaction = () => { const overmind = React.useContext(context); if (!overmind.mode) { throwMissingContextError(); } return overmind.reaction; }; export const createStateHook = () => // eslint-disable-next-line dot-notation typeof React['useSyncExternalStore'] === 'function' ? useStateV18 : useState; export const createActionsHook = () => { return useActions; }; export const createEffectsHook = () => { return useEffects; }; export const createReactionHook = () => { return useReaction; }; //# sourceMappingURL=index.js.map