@zedux/react
Version:
A Molecular State Engine for React
119 lines (118 loc) • 5.49 kB
JavaScript
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;
};