overmind-react
Version:
Functional actions
259 lines • 8.88 kB
JavaScript
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