@zedux/react
Version:
A Molecular State Engine for React
176 lines (145 loc) • 5.9 kB
text/typescript
import {
AtomSelectorConfig,
AtomSelectorOrConfig,
DependentEdge,
haveDepsChanged,
SelectorCache,
} from '@zedux/atoms'
import { useEffect, useState } from 'react'
import { External } from '../utils'
import { useEcosystem } from './useEcosystem'
import { useReactComponentId } from './useReactComponentId'
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 = <T, Args extends any[]>(
selectorOrConfig: AtomSelectorOrConfig<T, Args>,
...args: Args
): T => {
const ecosystem = useEcosystem()
const { _graph, selectors } = ecosystem
const dependentKey = useReactComponentId()
const [, render] = useState<undefined | object>()
const existingCache = (render as any).cache as
| SelectorCache<T, Args>
| undefined
const argsChanged =
!existingCache ||
((selectorOrConfig as AtomSelectorConfig<T, Args>).argsComparator
? !(
(selectorOrConfig as AtomSelectorConfig<T, Args>).argsComparator as (
newArgs: Args,
oldArgs: Args
) => boolean
)(args, existingCache.args || ([] as unknown as Args))
: haveDepsChanged(existingCache.args, args))
const resolvedArgs = argsChanged ? args : (existingCache.args as 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
? _graph.nodes[existingCache.id]?.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 as any).mounted = false
selectors._swapRefs(
existingCache as SelectorCache<any, any[]>,
selectorOrConfig as AtomSelectorOrConfig<any, any[]>,
resolvedArgs
)
;(render as any).mounted = true
}
let cache = isSwappingRefs
? (existingCache as SelectorCache<T, Args>)
: selectors.getCache(selectorOrConfig, resolvedArgs)
let edge: DependentEdge | undefined
const addEdge = (isMaterialized?: boolean) => {
if (!_graph.nodes[cache.id]?.dependents.get(dependentKey)) {
edge = _graph.addEdge(dependentKey, cache.id, OPERATION, External, () => {
if ((render as any).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 as any).cache = cache as SelectorCache<any, any[]>
useEffect(() => {
cache = isSwappingRefs
? (existingCache as SelectorCache<T, Args>)
: selectors.getCache(selectorOrConfig, resolvedArgs)
if (edge) {
let prevEdge = edge.prevEdge?.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 = prevEdge.prevEdge?.deref()
}
}
let prevCache = cache._prevCache?.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 = prevCache._prevCache?.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)
// use the referentially stable render function as a ref :O
;(render as any).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 as T
}