@wordpress/data 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/LICENSE.md +1 -1
- package/build/components/use-select/index.js +139 -239
- package/build/components/use-select/index.js.map +1 -1
- package/build-module/components/use-select/index.js +140 -239
- package/build-module/components/use-select/index.js.map +1 -1
- package/build-types/components/use-select/index.d.ts +0 -13
- package/build-types/components/use-select/index.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/components/use-select/index.js +142 -275
- package/src/components/use-select/test/index.js +18 -20
- package/src/components/use-select/test/suspense.js +72 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External dependencies
|
|
3
|
-
*/
|
|
4
|
-
import { useMemoOne } from 'use-memo-one';
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* WordPress dependencies
|
|
8
3
|
*/
|
|
@@ -11,11 +6,10 @@ import {
|
|
|
11
6
|
useRef,
|
|
12
7
|
useCallback,
|
|
13
8
|
useMemo,
|
|
14
|
-
|
|
9
|
+
useSyncExternalStore,
|
|
15
10
|
useDebugValue,
|
|
16
11
|
} from '@wordpress/element';
|
|
17
12
|
import isShallowEqual from '@wordpress/is-shallow-equal';
|
|
18
|
-
import { useIsomorphicLayoutEffect } from '@wordpress/compose';
|
|
19
13
|
|
|
20
14
|
/**
|
|
21
15
|
* Internal dependencies
|
|
@@ -23,7 +17,6 @@ import { useIsomorphicLayoutEffect } from '@wordpress/compose';
|
|
|
23
17
|
import useRegistry from '../registry-provider/use-registry';
|
|
24
18
|
import useAsyncMode from '../async-mode-provider/use-async-mode';
|
|
25
19
|
|
|
26
|
-
const noop = () => {};
|
|
27
20
|
const renderQueue = createQueue();
|
|
28
21
|
|
|
29
22
|
/**
|
|
@@ -40,6 +33,128 @@ const renderQueue = createQueue();
|
|
|
40
33
|
*/
|
|
41
34
|
/** @typedef {import('../../types').MapSelect} MapSelect */
|
|
42
35
|
|
|
36
|
+
function Store( registry, suspense ) {
|
|
37
|
+
const select = suspense ? registry.suspendSelect : registry.select;
|
|
38
|
+
const queueContext = {};
|
|
39
|
+
let lastMapSelect;
|
|
40
|
+
let lastMapResult;
|
|
41
|
+
let lastMapResultValid = false;
|
|
42
|
+
let lastIsAsync;
|
|
43
|
+
let subscribe;
|
|
44
|
+
|
|
45
|
+
const createSubscriber = ( stores ) => ( listener ) => {
|
|
46
|
+
// Invalidate the value right after subscription was created. React will
|
|
47
|
+
// call `getValue` after subscribing, to detect store updates that happened
|
|
48
|
+
// in the interval between the `getValue` call during render and creating
|
|
49
|
+
// the subscription, which is slightly delayed. We need to ensure that this
|
|
50
|
+
// second `getValue` call will compute a fresh value.
|
|
51
|
+
lastMapResultValid = false;
|
|
52
|
+
|
|
53
|
+
const onStoreChange = () => {
|
|
54
|
+
// Invalidate the value on store update, so that a fresh value is computed.
|
|
55
|
+
lastMapResultValid = false;
|
|
56
|
+
listener();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onChange = () => {
|
|
60
|
+
if ( lastIsAsync ) {
|
|
61
|
+
renderQueue.add( queueContext, onStoreChange );
|
|
62
|
+
} else {
|
|
63
|
+
onStoreChange();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const unsubs = stores.map( ( storeName ) => {
|
|
68
|
+
return registry.subscribe( onChange, storeName );
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
// The return value of the subscribe function could be undefined if the store is a custom generic store.
|
|
73
|
+
for ( const unsub of unsubs ) {
|
|
74
|
+
unsub?.();
|
|
75
|
+
}
|
|
76
|
+
// Cancel existing store updates that were already scheduled.
|
|
77
|
+
renderQueue.cancel( queueContext );
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return ( mapSelect, resubscribe, isAsync ) => {
|
|
82
|
+
const selectValue = () => mapSelect( select, registry );
|
|
83
|
+
|
|
84
|
+
function updateValue( selectFromStore ) {
|
|
85
|
+
// If the last value is valid, and the `mapSelect` callback hasn't changed,
|
|
86
|
+
// then we can safely return the cached value. The value can change only on
|
|
87
|
+
// store update, and in that case value will be invalidated by the listener.
|
|
88
|
+
if ( lastMapResultValid && mapSelect === lastMapSelect ) {
|
|
89
|
+
return lastMapResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mapResult = selectFromStore();
|
|
93
|
+
|
|
94
|
+
// If the new value is shallow-equal to the old one, keep the old one so
|
|
95
|
+
// that we don't trigger unwanted updates that do a `===` check.
|
|
96
|
+
if ( ! isShallowEqual( lastMapResult, mapResult ) ) {
|
|
97
|
+
lastMapResult = mapResult;
|
|
98
|
+
}
|
|
99
|
+
lastMapResultValid = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getValue() {
|
|
103
|
+
// Update the value in case it's been invalidated or `mapSelect` has changed.
|
|
104
|
+
updateValue( selectValue );
|
|
105
|
+
return lastMapResult;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// When transitioning from async to sync mode, cancel existing store updates
|
|
109
|
+
// that have been scheduled, and invalidate the value so that it's freshly
|
|
110
|
+
// computed. It might have been changed by the update we just cancelled.
|
|
111
|
+
if ( lastIsAsync && ! isAsync ) {
|
|
112
|
+
lastMapResultValid = false;
|
|
113
|
+
renderQueue.cancel( queueContext );
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Either initialize the `subscribe` function, or create a new one if `mapSelect`
|
|
117
|
+
// changed and has dependencies.
|
|
118
|
+
// Usage without dependencies, `useSelect( ( s ) => { ... } )`, will subscribe
|
|
119
|
+
// only once, at mount, and won't resubscibe even if `mapSelect` changes.
|
|
120
|
+
if ( ! subscribe || ( resubscribe && mapSelect !== lastMapSelect ) ) {
|
|
121
|
+
// Find out what stores the `mapSelect` callback is selecting from and
|
|
122
|
+
// use that list to create subscriptions to specific stores.
|
|
123
|
+
const listeningStores = { current: null };
|
|
124
|
+
updateValue( () =>
|
|
125
|
+
registry.__unstableMarkListeningStores(
|
|
126
|
+
selectValue,
|
|
127
|
+
listeningStores
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
subscribe = createSubscriber( listeningStores.current );
|
|
131
|
+
} else {
|
|
132
|
+
updateValue( selectValue );
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
lastIsAsync = isAsync;
|
|
136
|
+
lastMapSelect = mapSelect;
|
|
137
|
+
|
|
138
|
+
// Return a pair of functions that can be passed to `useSyncExternalStore`.
|
|
139
|
+
return { subscribe, getValue };
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function useStaticSelect( storeName ) {
|
|
144
|
+
return useRegistry().select( storeName );
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function useMappingSelect( suspense, mapSelect, deps ) {
|
|
148
|
+
const registry = useRegistry();
|
|
149
|
+
const isAsync = useAsyncMode();
|
|
150
|
+
const store = useMemo( () => Store( registry, suspense ), [ registry ] );
|
|
151
|
+
const selector = useCallback( mapSelect, deps );
|
|
152
|
+
const { subscribe, getValue } = store( selector, !! deps, isAsync );
|
|
153
|
+
const result = useSyncExternalStore( subscribe, getValue, getValue );
|
|
154
|
+
useDebugValue( result );
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
43
158
|
/**
|
|
44
159
|
* Custom react hook for retrieving props from registered selectors.
|
|
45
160
|
*
|
|
@@ -105,162 +220,26 @@ const renderQueue = createQueue();
|
|
|
105
220
|
* @return {UseSelectReturn<T>} A custom react hook.
|
|
106
221
|
*/
|
|
107
222
|
export default function useSelect( mapSelect, deps ) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// on every invocation whether or not we have a real function
|
|
119
|
-
// for `mapSelect`. we'll create this intermediate variable to
|
|
120
|
-
// fulfill that need and then reference it with our "real"
|
|
121
|
-
// `_mapSelect` if we can.
|
|
122
|
-
const callbackMapper = useCallback(
|
|
123
|
-
hasMappingFunction ? mapSelect : noop,
|
|
124
|
-
deps
|
|
125
|
-
);
|
|
126
|
-
const _mapSelect = hasMappingFunction ? callbackMapper : null;
|
|
127
|
-
|
|
128
|
-
const registry = useRegistry();
|
|
129
|
-
const isAsync = useAsyncMode();
|
|
130
|
-
|
|
131
|
-
const latestRegistry = useRef( registry );
|
|
132
|
-
const latestMapSelect = useRef();
|
|
133
|
-
const latestIsAsync = useRef( isAsync );
|
|
134
|
-
const latestMapOutput = useRef();
|
|
135
|
-
const latestMapOutputError = useRef();
|
|
136
|
-
|
|
137
|
-
// Keep track of the stores being selected in the _mapSelect function,
|
|
138
|
-
// and only subscribe to those stores later.
|
|
139
|
-
const listeningStores = useRef( [] );
|
|
140
|
-
const wrapSelect = useCallback(
|
|
141
|
-
( callback ) =>
|
|
142
|
-
registry.__unstableMarkListeningStores(
|
|
143
|
-
() => callback( registry.select, registry ),
|
|
144
|
-
listeningStores
|
|
145
|
-
),
|
|
146
|
-
[ registry ]
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
// Generate a "flag" for used in the effect dependency array.
|
|
150
|
-
// It's different than just using `mapSelect` since deps could be undefined,
|
|
151
|
-
// in that case, we would still want to memoize it.
|
|
152
|
-
const depsChangedFlag = useMemo( () => ( {} ), deps || [] );
|
|
153
|
-
|
|
154
|
-
let mapOutput;
|
|
155
|
-
|
|
156
|
-
let selectorRan = false;
|
|
157
|
-
if ( _mapSelect ) {
|
|
158
|
-
mapOutput = latestMapOutput.current;
|
|
159
|
-
const hasReplacedRegistry = latestRegistry.current !== registry;
|
|
160
|
-
const hasReplacedMapSelect = latestMapSelect.current !== _mapSelect;
|
|
161
|
-
const hasLeftAsyncMode = latestIsAsync.current && ! isAsync;
|
|
162
|
-
const lastMapSelectFailed = !! latestMapOutputError.current;
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
hasReplacedRegistry ||
|
|
166
|
-
hasReplacedMapSelect ||
|
|
167
|
-
hasLeftAsyncMode ||
|
|
168
|
-
lastMapSelectFailed
|
|
169
|
-
) {
|
|
170
|
-
try {
|
|
171
|
-
mapOutput = wrapSelect( _mapSelect );
|
|
172
|
-
selectorRan = true;
|
|
173
|
-
} catch ( error ) {
|
|
174
|
-
let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`;
|
|
175
|
-
|
|
176
|
-
if ( latestMapOutputError.current ) {
|
|
177
|
-
errorMessage += `\nThe error may be correlated with this previous error:\n`;
|
|
178
|
-
errorMessage += `${ latestMapOutputError.current.stack }\n\n`;
|
|
179
|
-
errorMessage += 'Original stack trace:';
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// eslint-disable-next-line no-console
|
|
183
|
-
console.error( errorMessage );
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
useIsomorphicLayoutEffect( () => {
|
|
189
|
-
if ( ! hasMappingFunction ) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
latestRegistry.current = registry;
|
|
194
|
-
latestMapSelect.current = _mapSelect;
|
|
195
|
-
latestIsAsync.current = isAsync;
|
|
196
|
-
if ( selectorRan ) {
|
|
197
|
-
latestMapOutput.current = mapOutput;
|
|
198
|
-
}
|
|
199
|
-
latestMapOutputError.current = undefined;
|
|
200
|
-
} );
|
|
201
|
-
|
|
202
|
-
// React can sometimes clear the `useMemo` cache.
|
|
203
|
-
// We use the cache-stable `useMemoOne` to avoid
|
|
204
|
-
// losing queues.
|
|
205
|
-
const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
|
|
206
|
-
const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
|
|
207
|
-
const isMounted = useRef( false );
|
|
208
|
-
|
|
209
|
-
useIsomorphicLayoutEffect( () => {
|
|
210
|
-
if ( ! hasMappingFunction ) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const onStoreChange = () => {
|
|
215
|
-
try {
|
|
216
|
-
const newMapOutput = wrapSelect( latestMapSelect.current );
|
|
217
|
-
|
|
218
|
-
if ( isShallowEqual( latestMapOutput.current, newMapOutput ) ) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
latestMapOutput.current = newMapOutput;
|
|
222
|
-
} catch ( error ) {
|
|
223
|
-
latestMapOutputError.current = error;
|
|
224
|
-
}
|
|
225
|
-
forceRender();
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const onChange = () => {
|
|
229
|
-
if ( ! isMounted.current ) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if ( latestIsAsync.current ) {
|
|
234
|
-
renderQueue.add( queueContext, onStoreChange );
|
|
235
|
-
} else {
|
|
236
|
-
onStoreChange();
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// Catch any possible state changes during mount before the subscription
|
|
241
|
-
// could be set.
|
|
242
|
-
onStoreChange();
|
|
243
|
-
|
|
244
|
-
const unsubscribers = listeningStores.current.map( ( storeName ) =>
|
|
245
|
-
registry.subscribe( onChange, storeName )
|
|
223
|
+
// On initial call, on mount, determine the mode of this `useSelect` call
|
|
224
|
+
// and then never allow it to change on subsequent updates.
|
|
225
|
+
const staticSelectMode = typeof mapSelect !== 'function';
|
|
226
|
+
const staticSelectModeRef = useRef( staticSelectMode );
|
|
227
|
+
|
|
228
|
+
if ( staticSelectMode !== staticSelectModeRef.current ) {
|
|
229
|
+
const prevMode = staticSelectModeRef.current ? 'static' : 'mapping';
|
|
230
|
+
const nextMode = staticSelectMode ? 'static' : 'mapping';
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Switching useSelect from ${ prevMode } to ${ nextMode } is not allowed`
|
|
246
233
|
);
|
|
234
|
+
}
|
|
247
235
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
};
|
|
256
|
-
// If you're tempted to eliminate the spread dependencies below don't do it!
|
|
257
|
-
// We're passing these in from the calling function and want to make sure we're
|
|
258
|
-
// examining every individual value inside the `deps` array.
|
|
259
|
-
}, [ registry, wrapSelect, hasMappingFunction, depsChangedFlag ] );
|
|
260
|
-
|
|
261
|
-
useDebugValue( mapOutput );
|
|
262
|
-
|
|
263
|
-
return hasMappingFunction ? mapOutput : registry.select( mapSelect );
|
|
236
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
237
|
+
// `staticSelectMode` is not allowed to change during the hook instance's,
|
|
238
|
+
// lifetime, so the rules of hooks are not really violated.
|
|
239
|
+
return staticSelectMode
|
|
240
|
+
? useStaticSelect( mapSelect )
|
|
241
|
+
: useMappingSelect( false, mapSelect, deps );
|
|
242
|
+
/* eslint-enable react-hooks/rules-of-hooks */
|
|
264
243
|
}
|
|
265
244
|
|
|
266
245
|
/**
|
|
@@ -279,117 +258,5 @@ export default function useSelect( mapSelect, deps ) {
|
|
|
279
258
|
* @return {Object} Data object returned by the `mapSelect` function.
|
|
280
259
|
*/
|
|
281
260
|
export function useSuspenseSelect( mapSelect, deps ) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const registry = useRegistry();
|
|
285
|
-
const isAsync = useAsyncMode();
|
|
286
|
-
|
|
287
|
-
const latestRegistry = useRef( registry );
|
|
288
|
-
const latestMapSelect = useRef();
|
|
289
|
-
const latestIsAsync = useRef( isAsync );
|
|
290
|
-
const latestMapOutput = useRef();
|
|
291
|
-
const latestMapOutputError = useRef();
|
|
292
|
-
|
|
293
|
-
// Keep track of the stores being selected in the `mapSelect` function,
|
|
294
|
-
// and only subscribe to those stores later.
|
|
295
|
-
const listeningStores = useRef( [] );
|
|
296
|
-
const wrapSelect = useCallback(
|
|
297
|
-
( callback ) =>
|
|
298
|
-
registry.__unstableMarkListeningStores(
|
|
299
|
-
() => callback( registry.suspendSelect, registry ),
|
|
300
|
-
listeningStores
|
|
301
|
-
),
|
|
302
|
-
[ registry ]
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
// Generate a "flag" for used in the effect dependency array.
|
|
306
|
-
// It's different than just using `mapSelect` since deps could be undefined,
|
|
307
|
-
// in that case, we would still want to memoize it.
|
|
308
|
-
const depsChangedFlag = useMemo( () => ( {} ), deps || [] );
|
|
309
|
-
|
|
310
|
-
let mapOutput = latestMapOutput.current;
|
|
311
|
-
let mapOutputError = latestMapOutputError.current;
|
|
312
|
-
|
|
313
|
-
const hasReplacedRegistry = latestRegistry.current !== registry;
|
|
314
|
-
const hasReplacedMapSelect = latestMapSelect.current !== _mapSelect;
|
|
315
|
-
const hasLeftAsyncMode = latestIsAsync.current && ! isAsync;
|
|
316
|
-
|
|
317
|
-
let selectorRan = false;
|
|
318
|
-
if ( hasReplacedRegistry || hasReplacedMapSelect || hasLeftAsyncMode ) {
|
|
319
|
-
try {
|
|
320
|
-
mapOutput = wrapSelect( _mapSelect );
|
|
321
|
-
selectorRan = true;
|
|
322
|
-
} catch ( error ) {
|
|
323
|
-
mapOutputError = error;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
useIsomorphicLayoutEffect( () => {
|
|
328
|
-
latestRegistry.current = registry;
|
|
329
|
-
latestMapSelect.current = _mapSelect;
|
|
330
|
-
latestIsAsync.current = isAsync;
|
|
331
|
-
if ( selectorRan ) {
|
|
332
|
-
latestMapOutput.current = mapOutput;
|
|
333
|
-
}
|
|
334
|
-
latestMapOutputError.current = mapOutputError;
|
|
335
|
-
} );
|
|
336
|
-
|
|
337
|
-
// React can sometimes clear the `useMemo` cache.
|
|
338
|
-
// We use the cache-stable `useMemoOne` to avoid
|
|
339
|
-
// losing queues.
|
|
340
|
-
const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
|
|
341
|
-
const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
|
|
342
|
-
const isMounted = useRef( false );
|
|
343
|
-
|
|
344
|
-
useIsomorphicLayoutEffect( () => {
|
|
345
|
-
const onStoreChange = () => {
|
|
346
|
-
try {
|
|
347
|
-
const newMapOutput = wrapSelect( latestMapSelect.current );
|
|
348
|
-
|
|
349
|
-
if ( isShallowEqual( latestMapOutput.current, newMapOutput ) ) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
latestMapOutput.current = newMapOutput;
|
|
353
|
-
} catch ( error ) {
|
|
354
|
-
latestMapOutputError.current = error;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
forceRender();
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const onChange = () => {
|
|
361
|
-
if ( ! isMounted.current ) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if ( latestIsAsync.current ) {
|
|
366
|
-
renderQueue.add( queueContext, onStoreChange );
|
|
367
|
-
} else {
|
|
368
|
-
onStoreChange();
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// catch any possible state changes during mount before the subscription
|
|
373
|
-
// could be set.
|
|
374
|
-
onStoreChange();
|
|
375
|
-
|
|
376
|
-
const unsubscribers = listeningStores.current.map( ( storeName ) =>
|
|
377
|
-
registry.subscribe( onChange, storeName )
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
isMounted.current = true;
|
|
381
|
-
|
|
382
|
-
return () => {
|
|
383
|
-
// The return value of the subscribe function could be undefined if the store is a custom generic store.
|
|
384
|
-
unsubscribers.forEach( ( unsubscribe ) => unsubscribe?.() );
|
|
385
|
-
renderQueue.cancel( queueContext );
|
|
386
|
-
isMounted.current = false;
|
|
387
|
-
};
|
|
388
|
-
}, [ registry, wrapSelect, depsChangedFlag ] );
|
|
389
|
-
|
|
390
|
-
if ( mapOutputError ) {
|
|
391
|
-
throw mapOutputError;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return mapOutput;
|
|
261
|
+
return useMappingSelect( true, mapSelect, deps );
|
|
395
262
|
}
|
|
@@ -50,9 +50,9 @@ describe( 'useSelect', () => {
|
|
|
50
50
|
</RegistryProvider>
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
// 2
|
|
53
|
+
// 2 selectSpy calls expected
|
|
54
54
|
// - 1 for initial mount
|
|
55
|
-
// - 1 for
|
|
55
|
+
// - 1 for the subscription effect checking if value has changed
|
|
56
56
|
expect( selectSpy ).toHaveBeenCalledTimes( 2 );
|
|
57
57
|
expect( TestComponent ).toHaveBeenCalledTimes( 1 );
|
|
58
58
|
|
|
@@ -118,8 +118,7 @@ describe( 'useSelect', () => {
|
|
|
118
118
|
expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'bar' );
|
|
119
119
|
} );
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
it.skip( 'avoid calling nested listener after unmounted', async () => {
|
|
121
|
+
it( 'does not rerender a nested component that is to be unmounted', () => {
|
|
123
122
|
registry.registerStore( 'toggler', {
|
|
124
123
|
reducer: ( state = false, action ) =>
|
|
125
124
|
action.type === 'TOGGLE' ? ! state : state,
|
|
@@ -134,16 +133,16 @@ describe( 'useSelect', () => {
|
|
|
134
133
|
const mapSelect = ( select ) => select( 'toggler' ).get();
|
|
135
134
|
|
|
136
135
|
const mapSelectChild = jest.fn( mapSelect );
|
|
137
|
-
|
|
136
|
+
const Child = jest.fn( () => {
|
|
138
137
|
const show = useSelect( mapSelectChild, [] );
|
|
139
138
|
return show ? 'yes' : 'no';
|
|
140
|
-
}
|
|
139
|
+
} );
|
|
141
140
|
|
|
142
141
|
const mapSelectParent = jest.fn( mapSelect );
|
|
143
|
-
|
|
142
|
+
const Parent = jest.fn( () => {
|
|
144
143
|
const show = useSelect( mapSelectParent, [] );
|
|
145
144
|
return show ? <Child /> : 'none';
|
|
146
|
-
}
|
|
145
|
+
} );
|
|
147
146
|
|
|
148
147
|
render(
|
|
149
148
|
<RegistryProvider value={ registry }>
|
|
@@ -155,14 +154,10 @@ describe( 'useSelect', () => {
|
|
|
155
154
|
expect( screen.getByText( 'none' ) ).toBeInTheDocument();
|
|
156
155
|
expect( mapSelectParent ).toHaveBeenCalledTimes( 2 );
|
|
157
156
|
expect( mapSelectChild ).toHaveBeenCalledTimes( 0 );
|
|
157
|
+
expect( Parent ).toHaveBeenCalledTimes( 1 );
|
|
158
|
+
expect( Child ).toHaveBeenCalledTimes( 0 );
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
// will be executed only after the dispatch finishes. But we want to opt out of
|
|
161
|
-
// batched updates here. We want all the setStates to be done synchronously, as the
|
|
162
|
-
// store listeners are called. The async/await code is a trick to do it: do the
|
|
163
|
-
// dispatch in a different event loop tick, where the batched updates are no longer active.
|
|
164
|
-
await act( async () => {
|
|
165
|
-
await Promise.resolve();
|
|
160
|
+
act( () => {
|
|
166
161
|
registry.dispatch( 'toggler' ).toggle();
|
|
167
162
|
} );
|
|
168
163
|
|
|
@@ -170,18 +165,21 @@ describe( 'useSelect', () => {
|
|
|
170
165
|
expect( screen.getByText( 'yes' ) ).toBeInTheDocument();
|
|
171
166
|
expect( mapSelectParent ).toHaveBeenCalledTimes( 3 );
|
|
172
167
|
expect( mapSelectChild ).toHaveBeenCalledTimes( 2 );
|
|
168
|
+
expect( Parent ).toHaveBeenCalledTimes( 2 );
|
|
169
|
+
expect( Child ).toHaveBeenCalledTimes( 1 );
|
|
173
170
|
|
|
174
|
-
|
|
175
|
-
await Promise.resolve();
|
|
171
|
+
act( () => {
|
|
176
172
|
registry.dispatch( 'toggler' ).toggle();
|
|
177
173
|
} );
|
|
178
174
|
|
|
179
175
|
// Check that child was unmounted without any extra state update being performed on it.
|
|
180
|
-
// I.e., `mapSelectChild` was
|
|
181
|
-
//
|
|
176
|
+
// I.e., `mapSelectChild` was called again, and state update was scheduled, we cannot
|
|
177
|
+
// avoid that, but the state update is never executed and doesn't do a rerender.
|
|
182
178
|
expect( screen.getByText( 'none' ) ).toBeInTheDocument();
|
|
183
179
|
expect( mapSelectParent ).toHaveBeenCalledTimes( 4 );
|
|
184
|
-
expect( mapSelectChild ).toHaveBeenCalledTimes(
|
|
180
|
+
expect( mapSelectChild ).toHaveBeenCalledTimes( 3 );
|
|
181
|
+
expect( Parent ).toHaveBeenCalledTimes( 3 );
|
|
182
|
+
expect( Child ).toHaveBeenCalledTimes( 1 );
|
|
185
183
|
} );
|
|
186
184
|
|
|
187
185
|
describe( 'rerenders as expected with various mapSelect return types', () => {
|
|
@@ -162,4 +162,76 @@ describe( 'useSuspenseSelect', () => {
|
|
|
162
162
|
expect( label ).toHaveTextContent( 'resolution failed' );
|
|
163
163
|
expect( console ).toHaveErrored();
|
|
164
164
|
} );
|
|
165
|
+
|
|
166
|
+
it( 'independent resolutions do not cause unrelated rerenders', async () => {
|
|
167
|
+
const store = createReduxStore( 'test', {
|
|
168
|
+
reducer: ( state = {}, action ) => {
|
|
169
|
+
switch ( action.type ) {
|
|
170
|
+
case 'RECEIVE':
|
|
171
|
+
return { ...state, [ action.endpoint ]: action.data };
|
|
172
|
+
default:
|
|
173
|
+
return state;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
selectors: {
|
|
177
|
+
getData: ( state, endpoint ) => state[ endpoint ],
|
|
178
|
+
},
|
|
179
|
+
resolvers: {
|
|
180
|
+
getData:
|
|
181
|
+
( endpoint ) =>
|
|
182
|
+
async ( { dispatch } ) => {
|
|
183
|
+
const delay = endpoint === 'slow' ? 30 : 10;
|
|
184
|
+
await new Promise( ( r ) =>
|
|
185
|
+
setTimeout( () => r(), delay )
|
|
186
|
+
);
|
|
187
|
+
dispatch( {
|
|
188
|
+
type: 'RECEIVE',
|
|
189
|
+
endpoint,
|
|
190
|
+
data: endpoint,
|
|
191
|
+
} );
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
} );
|
|
195
|
+
|
|
196
|
+
const registry = createRegistry();
|
|
197
|
+
registry.register( store );
|
|
198
|
+
|
|
199
|
+
const FastUI = jest.fn( () => {
|
|
200
|
+
const data = useSuspenseSelect(
|
|
201
|
+
( select ) => select( store ).getData( 'fast' ),
|
|
202
|
+
[]
|
|
203
|
+
);
|
|
204
|
+
return <div aria-label="fast loaded">{ data }</div>;
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
const SlowUI = jest.fn( () => {
|
|
208
|
+
const data = useSuspenseSelect(
|
|
209
|
+
( select ) => select( store ).getData( 'slow' ),
|
|
210
|
+
[]
|
|
211
|
+
);
|
|
212
|
+
return <div aria-label="slow loaded">{ data }</div>;
|
|
213
|
+
} );
|
|
214
|
+
|
|
215
|
+
const App = () => (
|
|
216
|
+
<RegistryProvider value={ registry }>
|
|
217
|
+
<Suspense fallback="fast loading">
|
|
218
|
+
<FastUI />
|
|
219
|
+
</Suspense>
|
|
220
|
+
<Suspense fallback="slow loading">
|
|
221
|
+
<SlowUI />
|
|
222
|
+
</Suspense>
|
|
223
|
+
</RegistryProvider>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
render( <App /> );
|
|
227
|
+
|
|
228
|
+
const fastLabel = await screen.findByLabelText( 'fast loaded' );
|
|
229
|
+
expect( fastLabel ).toHaveTextContent( 'fast' );
|
|
230
|
+
|
|
231
|
+
const slowLabel = await screen.findByLabelText( 'slow loaded' );
|
|
232
|
+
expect( slowLabel ).toHaveTextContent( 'slow' );
|
|
233
|
+
|
|
234
|
+
expect( FastUI ).toHaveBeenCalledTimes( 2 );
|
|
235
|
+
expect( SlowUI ).toHaveBeenCalledTimes( 2 );
|
|
236
|
+
} );
|
|
165
237
|
} );
|