@zedux/react
Version:
A Molecular State Engine for React
128 lines (127 loc) • 6.01 kB
JavaScript
import { haveDepsChanged, } from '@zedux/atoms';
import { useEffect, useState } from 'react';
import { External } from '../utils.js';
import { useEcosystem } from './useEcosystem.js';
import { useReactComponentId } from './useReactComponentId.js';
const OPERATION = 'useAtomSelector';
/**
* Get the result of running an AtomSelector in the current ecosystem.
*
* If the exact selector function (or object if it's an AtomSelectorConfig
* object) reference + params combo has been used in this ecosystem before,
* return the cached result.
*
* Register a dynamic graph dependency between this React component (as a new
* external node) and the AtomSelector.
*/
export const useAtomSelector = (selectorOrConfig, ...args) => {
var _a;
const ecosystem = useEcosystem();
const { _graph, selectors } = ecosystem;
const dependentKey = useReactComponentId();
const [, render] = useState();
const existingCache = render.cache;
const argsChanged = !existingCache ||
(selectorOrConfig.argsComparator
? !selectorOrConfig.argsComparator(args, existingCache.args || [])
: haveDepsChanged(existingCache.args, args));
const resolvedArgs = argsChanged ? args : existingCache.args;
// if the refs/args don't match, existingCache has refCount: 1, there is no
// cache yet for the new ref, and the new ref has the same name, assume it's
// an inline selector
const isSwappingRefs = existingCache &&
existingCache.selectorRef !== selectorOrConfig &&
!argsChanged
? ((_a = _graph.nodes[existingCache.id]) === null || _a === void 0 ? void 0 : _a.refCount) === 1 &&
!selectors._refBaseKeys.has(selectorOrConfig) &&
selectors._getIdealCacheId(existingCache.selectorRef) ===
selectors._getIdealCacheId(selectorOrConfig)
: false;
if (isSwappingRefs) {
// switch `mounted` to false temporarily to prevent circular rerenders
;
render.mounted = false;
selectors._swapRefs(existingCache, selectorOrConfig, resolvedArgs);
render.mounted = true;
}
let cache = isSwappingRefs
? existingCache
: selectors.getCache(selectorOrConfig, resolvedArgs);
let edge;
const addEdge = (isMaterialized) => {
var _a;
if (!((_a = _graph.nodes[cache.id]) === null || _a === void 0 ? void 0 : _a.dependents.get(dependentKey))) {
edge = _graph.addEdge(dependentKey, cache.id, OPERATION, External, () => {
if (render.mounted)
render({});
});
if (edge) {
edge.isMaterialized = isMaterialized;
edge.dependentKey = dependentKey;
if (cache._lastEdge) {
edge.prevEdge = cache._lastEdge;
}
cache._lastEdge = new WeakRef(edge);
}
if (selectors._lastCache) {
cache._prevCache = selectors._lastCache;
}
selectors._lastCache = new WeakRef(cache);
}
};
// Yes, subscribe during render. This operation is idempotent.
addEdge();
const renderedResult = cache.result;
render.cache = cache;
useEffect(() => {
var _a, _b, _c, _d;
cache = isSwappingRefs
? existingCache
: selectors.getCache(selectorOrConfig, resolvedArgs);
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, cache.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();
}
}
let prevCache = (_c = cache._prevCache) === null || _c === void 0 ? void 0 : _c.deref();
cache.isMaterialized = true;
// clear out any junk caches created by StrictMode
while (prevCache && !prevCache.isMaterialized) {
selectors.destroyCache(prevCache, [], true);
// mark in case of circular references (can happen in certain React
// rendering sequences)
prevCache.isMaterialized = true;
prevCache = (_d = prevCache._prevCache) === null || _d === void 0 ? void 0 : _d.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 force-destroy the selector
// or update the state of its dependencies (causing it to rerun) before we
// set `render.mounted`. If that happened, trigger a rerender to recreate
// the selector and/or get its new state
if (cache.isDestroyed || cache.result !== renderedResult) {
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 selectors and `ttl: 0` atoms with no other
// dependents get destroyed and recreated - that's part of what StrictMode
// is ensuring
_graph.removeEdge(dependentKey, cache.id);
// no need to set `render.mounted` to false here
};
}, [cache.id]);
return renderedResult;
};