/
useSelector.ts
169 lines (149 loc) · 5.31 KB
/
useSelector.ts
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
import {
computeSelector,
isObservable,
isPrimitive,
isPromise,
ListenerParams,
Observable,
Selector,
trackSelector,
when,
WithState,
} from '@legendapp/state';
import React, { useContext, useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';
import { reactGlobals } from './react-globals';
import type { UseSelectorOptions } from './reactInterfaces';
import { PauseContext } from './usePauseProvider';
interface SelectorFunctions<T> {
subscribe: (onStoreChange: () => void) => () => void;
getVersion: () => number;
run: (selector: Selector<T>) => T;
}
function createSelectorFunctions<T>(
options: UseSelectorOptions | undefined,
isPaused$: Observable<boolean>,
): SelectorFunctions<T> {
let version = 0;
let notify: () => void;
let dispose: (() => void) | undefined;
let resubscribe: (() => () => void) | undefined;
let _selector: Selector<T>;
let prev: T;
let pendingUpdate: any | undefined = undefined;
const run = () => {
// Dispose if already listening
dispose?.();
const {
value,
dispose: _dispose,
resubscribe: _resubscribe,
} = trackSelector(_selector, _update, undefined, undefined, /*createResubscribe*/ true);
dispose = _dispose;
resubscribe = _resubscribe;
return value;
};
const _update = ({ value }: { value: ListenerParams['value'] }) => {
if (isPaused$?.peek()) {
const next = pendingUpdate;
pendingUpdate = value;
if (next === undefined) {
when(
() => !isPaused$.get(),
() => {
const latest = pendingUpdate;
pendingUpdate = undefined;
_update({ value: latest });
},
);
}
} else {
// If skipCheck then don't need to re-run selector
let changed = options?.skipCheck;
if (!changed) {
const newValue = run();
// If newValue is different than previous value then it's changed.
// Also if the selector returns an observable directly then its value will be the same as
// the value from the listener, and that should always re-render.
if (newValue !== prev || (!isPrimitive(newValue) && newValue === value)) {
changed = true;
}
}
if (changed) {
version++;
notify?.();
}
}
};
return {
subscribe: (onStoreChange: () => void) => {
notify = onStoreChange;
// Workaround for React 18 running twice in dev (part 2)
if (
(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') &&
!dispose &&
resubscribe
) {
dispose = resubscribe();
}
return () => {
dispose?.();
dispose = undefined;
};
},
getVersion: () => version,
run: (selector: Selector<T>) => {
// Update the cached selector
_selector = selector;
return (prev = run());
},
};
}
export function useSelector<T>(selector: Selector<T>, options?: UseSelectorOptions): T {
// Short-circuit to skip creating the hook if selector is an observable
// and running in an observer. If selector is a function it needs to run in its own context.
if (reactGlobals.inObserver && isObservable(selector)) {
return computeSelector(selector);
}
let value;
try {
const isPaused$ = useContext(PauseContext);
const selectorFn = useMemo(() => createSelectorFunctions<T>(options, isPaused$), []);
const { subscribe, getVersion, run } = selectorFn;
// Run the selector
// Note: The selector needs to run on every render because it may have different results
// than the previous run if it uses local state
value = run(selector) as any;
useSyncExternalStore(subscribe, getVersion, getVersion);
// Suspense support
if (options?.suspense) {
// Note: Although it's not possible for an observable to be a promise, the selector may be a
// function that returns a Promise, so we handle that case too.
if (
isPromise(value) ||
(!value &&
isObservable(selector) &&
!(selector as unknown as Observable<WithState>).state.isLoaded.get())
) {
if (React.use) {
React.use(value);
} else {
throw value;
}
}
}
} catch (err: unknown) {
if (
(process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') &&
(err as Error)?.message?.includes('Rendered more')
) {
console.warn(
`[legend-state]: You may want to wrap this component in \`observer\` to fix the error of ${
(err as Error).message
}`,
);
}
throw err;
}
return value;
}