-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathuseAtomSelector.ts
More file actions
172 lines (142 loc) · 5.51 KB
/
useAtomSelector.ts
File metadata and controls
172 lines (142 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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 = () => {
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.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)
addEdge()
// 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
}