UNPKG

@zedux/react

Version:

A Molecular State Engine for React

119 lines (118 loc) 5.49 kB
import { useEffect, useState } from 'react'; import { External, Static } from '../utils.js'; import { useEcosystem } from './useEcosystem.js'; import { useReactComponentId } from './useReactComponentId.js'; const OPERATION = 'useAtomInstance'; /** * Creates an atom instance for the passed atom template based on the passed * params. If an instance has already been created for the passed params, reuses * the existing instance. * * Registers a static graph dependency on the atom instance. This means * components that use this hook will not rerender when this atom instance's * state changes. * * If the atom doesn't take params or an instance is passed, pass an empty array * for the 2nd param when you need to supply the 3rd `config` param. * * The 3rd `config` param is an object with these fields: * * - `operation` - Used for debugging. Pass a string to describe the reason for * creating this graph edge * - `subscribe` - Pass `subscribe: true` to make `useAtomInstance` create a * dynamic graph dependency instead * - `suspend` - Pass `suspend: false` to prevent this hook from triggering * React suspense if the resolved atom has a promise set * * Note that if the params are large, serializing them every render can cause * some overhead. * * @param atom The atom template to instantiate or reuse an instantiation of OR * an atom instance itself. * @param params The params for generating the instance's key. Required if an * atom template is passed that requires params. * @param config An object with optional `operation`, `subscribe`, and `suspend` * fields. */ export const useAtomInstance = (atom, params, { operation = OPERATION, subscribe, suspend } = { operation: OPERATION, }) => { const ecosystem = useEcosystem(); const dependentKey = useReactComponentId(); const [, render] = useState(); // It should be fine for this to run every render. It's possible to change // approaches if it is too heavy sometimes. But don't memoize this call: let instance = ecosystem.getInstance(atom, params); const renderedState = instance.getState(); let edge; const addEdge = (isMaterialized) => { var _a; if (!((_a = ecosystem._graph.nodes[instance.id]) === null || _a === void 0 ? void 0 : _a.dependents.get(dependentKey))) { edge = ecosystem._graph.addEdge(dependentKey, instance.id, operation, External | (subscribe ? 0 : Static), () => { if (render.mounted) render({}); }); if (edge) { edge.isMaterialized = isMaterialized; edge.dependentKey = dependentKey; if (instance._lastEdge) { edge.prevEdge = instance._lastEdge; } instance._lastEdge = new WeakRef(edge); } } }; // Yes, subscribe during render. This operation is idempotent and we handle // React's StrictMode specifically. addEdge(); // Only remove the graph edge when the instance id changes or on component // destruction. useEffect(() => { var _a, _b; // re-get the instance in case StrictMode destroys it instance = ecosystem.getInstance(atom, params); if (edge) { let prevEdge = (_a = edge.prevEdge) === null || _a === void 0 ? void 0 : _a.deref(); edge.isMaterialized = true; // clear out any junk edges added by StrictMode while (prevEdge && !prevEdge.isMaterialized) { ecosystem._graph.removeEdge(prevEdge.dependentKey, instance.id); // mark in case of circular references (shouldn't happen, but just for // consistency with the prevCache algorithm) prevEdge.isMaterialized = true; prevEdge = (_b = prevEdge.prevEdge) === null || _b === void 0 ? void 0 : _b.deref(); } } // Try adding the edge again (will be a no-op unless React's StrictMode ran // this effect's cleanup unnecessarily OR other effects in child components // cleaned up this component's edges before it could materialize them. // That's fine, just recreate them with `isMaterialized: true` now) addEdge(true); render.mounted = true; // an unmounting component's effect cleanup can update or force-destroy the // atom instance before this component is mounted. If that happened, trigger // a rerender to recreate the atom instance and/or get its new state if ((subscribe && instance.getState() !== renderedState) || instance.status === 'Destroyed') { render({}); } return () => { // remove the edge immediately - no need for a delay here. When StrictMode // double-invokes (invokes, then cleans up, then re-invokes) this effect, // it's expected that any `ttl: 0` atoms get destroyed and recreated - // that's part of what StrictMode is ensuring ecosystem._graph.removeEdge(dependentKey, instance.id); // no need to set `render.mounted` to false here }; }, [instance.id]); if (suspend !== false) { const status = instance._promiseStatus; if (status === 'loading') { throw instance.promise; } else if (status === 'error') { throw instance._promiseError; } } return instance; };