@wordpress/data 6.6.1 → 6.9.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.
@@ -112,17 +112,12 @@ export default function useSelect( mapSelect, deps ) {
112
112
 
113
113
  const registry = useRegistry();
114
114
  const isAsync = useAsyncMode();
115
- // React can sometimes clear the `useMemo` cache.
116
- // We use the cache-stable `useMemoOne` to avoid
117
- // losing queues.
118
- const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
119
- const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
120
115
 
116
+ const latestRegistry = useRef( registry );
121
117
  const latestMapSelect = useRef();
122
118
  const latestIsAsync = useRef( isAsync );
123
119
  const latestMapOutput = useRef();
124
120
  const latestMapOutputError = useRef();
125
- const isMountedAndNotUnsubscribing = useRef();
126
121
 
127
122
  // Keep track of the stores being selected in the _mapSelect function,
128
123
  // and only subscribe to those stores later.
@@ -145,10 +140,17 @@ export default function useSelect( mapSelect, deps ) {
145
140
 
146
141
  if ( _mapSelect ) {
147
142
  mapOutput = latestMapOutput.current;
143
+ const hasReplacedRegistry = latestRegistry.current !== registry;
148
144
  const hasReplacedMapSelect = latestMapSelect.current !== _mapSelect;
145
+ const hasLeftAsyncMode = latestIsAsync.current && ! isAsync;
149
146
  const lastMapSelectFailed = !! latestMapOutputError.current;
150
147
 
151
- if ( hasReplacedMapSelect || lastMapSelectFailed ) {
148
+ if (
149
+ hasReplacedRegistry ||
150
+ hasReplacedMapSelect ||
151
+ hasLeftAsyncMode ||
152
+ lastMapSelectFailed
153
+ ) {
152
154
  try {
153
155
  mapOutput = wrapSelect( _mapSelect );
154
156
  } catch ( error ) {
@@ -171,45 +173,44 @@ export default function useSelect( mapSelect, deps ) {
171
173
  return;
172
174
  }
173
175
 
176
+ latestRegistry.current = registry;
174
177
  latestMapSelect.current = _mapSelect;
178
+ latestIsAsync.current = isAsync;
175
179
  latestMapOutput.current = mapOutput;
176
180
  latestMapOutputError.current = undefined;
177
- isMountedAndNotUnsubscribing.current = true;
178
-
179
- // This has to run after the other ref updates
180
- // to avoid using stale values in the flushed
181
- // callbacks or potentially overwriting a
182
- // changed `latestMapOutput.current`.
183
- if ( latestIsAsync.current !== isAsync ) {
184
- latestIsAsync.current = isAsync;
185
- renderQueue.flush( queueContext );
186
- }
187
181
  } );
188
182
 
183
+ // React can sometimes clear the `useMemo` cache.
184
+ // We use the cache-stable `useMemoOne` to avoid
185
+ // losing queues.
186
+ const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
187
+ const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
188
+ const isMounted = useRef( false );
189
+
189
190
  useIsomorphicLayoutEffect( () => {
190
191
  if ( ! hasMappingFunction ) {
191
192
  return;
192
193
  }
193
194
 
194
195
  const onStoreChange = () => {
195
- if ( isMountedAndNotUnsubscribing.current ) {
196
- try {
197
- const newMapOutput = wrapSelect( latestMapSelect.current );
198
-
199
- if (
200
- isShallowEqual( latestMapOutput.current, newMapOutput )
201
- ) {
202
- return;
203
- }
204
- latestMapOutput.current = newMapOutput;
205
- } catch ( error ) {
206
- latestMapOutputError.current = error;
196
+ try {
197
+ const newMapOutput = wrapSelect( latestMapSelect.current );
198
+
199
+ if ( isShallowEqual( latestMapOutput.current, newMapOutput ) ) {
200
+ return;
207
201
  }
208
- forceRender();
202
+ latestMapOutput.current = newMapOutput;
203
+ } catch ( error ) {
204
+ latestMapOutputError.current = error;
209
205
  }
206
+ forceRender();
210
207
  };
211
208
 
212
209
  const onChange = () => {
210
+ if ( ! isMounted.current ) {
211
+ return;
212
+ }
213
+
213
214
  if ( latestIsAsync.current ) {
214
215
  renderQueue.add( queueContext, onStoreChange );
215
216
  } else {
@@ -219,17 +220,19 @@ export default function useSelect( mapSelect, deps ) {
219
220
 
220
221
  // Catch any possible state changes during mount before the subscription
221
222
  // could be set.
222
- onChange();
223
+ onStoreChange();
223
224
 
224
225
  const unsubscribers = listeningStores.current.map( ( storeName ) =>
225
226
  registry.__unstableSubscribeStore( storeName, onChange )
226
227
  );
227
228
 
229
+ isMounted.current = true;
230
+
228
231
  return () => {
229
- isMountedAndNotUnsubscribing.current = false;
230
232
  // The return value of the subscribe function could be undefined if the store is a custom generic store.
231
233
  unsubscribers.forEach( ( unsubscribe ) => unsubscribe?.() );
232
- renderQueue.flush( queueContext );
234
+ renderQueue.cancel( queueContext );
235
+ isMounted.current = false;
233
236
  };
234
237
  // If you're tempted to eliminate the spread dependencies below don't do it!
235
238
  // We're passing these in from the calling function and want to make sure we're
@@ -238,3 +241,130 @@ export default function useSelect( mapSelect, deps ) {
238
241
 
239
242
  return hasMappingFunction ? mapOutput : registry.select( mapSelect );
240
243
  }
244
+
245
+ /**
246
+ * A variant of the `useSelect` hook that has the same API, but will throw a
247
+ * suspense Promise if any of the called selectors is in an unresolved state.
248
+ *
249
+ * @param {Function} mapSelect Function called on every state change. The
250
+ * returned value is exposed to the component
251
+ * using this hook. The function receives the
252
+ * `registry.suspendSelect` method as the first
253
+ * argument and the `registry` as the second one.
254
+ * @param {Array} deps A dependency array used to memoize the `mapSelect`
255
+ * so that the same `mapSelect` is invoked on every
256
+ * state change unless the dependencies change.
257
+ *
258
+ * @return {Object} Data object returned by the `mapSelect` function.
259
+ */
260
+ export function useSuspenseSelect( mapSelect, deps ) {
261
+ const _mapSelect = useCallback( mapSelect, deps );
262
+
263
+ const registry = useRegistry();
264
+ const isAsync = useAsyncMode();
265
+
266
+ const latestRegistry = useRef( registry );
267
+ const latestMapSelect = useRef();
268
+ const latestIsAsync = useRef( isAsync );
269
+ const latestMapOutput = useRef();
270
+ const latestMapOutputError = useRef();
271
+
272
+ // Keep track of the stores being selected in the `mapSelect` function,
273
+ // and only subscribe to those stores later.
274
+ const listeningStores = useRef( [] );
275
+ const wrapSelect = useCallback(
276
+ ( callback ) =>
277
+ registry.__unstableMarkListeningStores(
278
+ () => callback( registry.suspendSelect, registry ),
279
+ listeningStores
280
+ ),
281
+ [ registry ]
282
+ );
283
+
284
+ // Generate a "flag" for used in the effect dependency array.
285
+ // It's different than just using `mapSelect` since deps could be undefined,
286
+ // in that case, we would still want to memoize it.
287
+ const depsChangedFlag = useMemo( () => ( {} ), deps || [] );
288
+
289
+ let mapOutput = latestMapOutput.current;
290
+ let mapOutputError = latestMapOutputError.current;
291
+
292
+ const hasReplacedRegistry = latestRegistry.current !== registry;
293
+ const hasReplacedMapSelect = latestMapSelect.current !== _mapSelect;
294
+ const hasLeftAsyncMode = latestIsAsync.current && ! isAsync;
295
+
296
+ if ( hasReplacedRegistry || hasReplacedMapSelect || hasLeftAsyncMode ) {
297
+ try {
298
+ mapOutput = wrapSelect( _mapSelect );
299
+ } catch ( error ) {
300
+ mapOutputError = error;
301
+ }
302
+ }
303
+
304
+ useIsomorphicLayoutEffect( () => {
305
+ latestRegistry.current = registry;
306
+ latestMapSelect.current = _mapSelect;
307
+ latestIsAsync.current = isAsync;
308
+ latestMapOutput.current = mapOutput;
309
+ latestMapOutputError.current = mapOutputError;
310
+ } );
311
+
312
+ // React can sometimes clear the `useMemo` cache.
313
+ // We use the cache-stable `useMemoOne` to avoid
314
+ // losing queues.
315
+ const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] );
316
+ const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 );
317
+ const isMounted = useRef( false );
318
+
319
+ useIsomorphicLayoutEffect( () => {
320
+ const onStoreChange = () => {
321
+ try {
322
+ const newMapOutput = wrapSelect( latestMapSelect.current );
323
+
324
+ if ( isShallowEqual( latestMapOutput.current, newMapOutput ) ) {
325
+ return;
326
+ }
327
+ latestMapOutput.current = newMapOutput;
328
+ } catch ( error ) {
329
+ latestMapOutputError.current = error;
330
+ }
331
+
332
+ forceRender();
333
+ };
334
+
335
+ const onChange = () => {
336
+ if ( ! isMounted.current ) {
337
+ return;
338
+ }
339
+
340
+ if ( latestIsAsync.current ) {
341
+ renderQueue.add( queueContext, onStoreChange );
342
+ } else {
343
+ onStoreChange();
344
+ }
345
+ };
346
+
347
+ // catch any possible state changes during mount before the subscription
348
+ // could be set.
349
+ onStoreChange();
350
+
351
+ const unsubscribers = listeningStores.current.map( ( storeName ) =>
352
+ registry.__unstableSubscribeStore( storeName, onChange )
353
+ );
354
+
355
+ isMounted.current = true;
356
+
357
+ return () => {
358
+ // The return value of the subscribe function could be undefined if the store is a custom generic store.
359
+ unsubscribers.forEach( ( unsubscribe ) => unsubscribe?.() );
360
+ renderQueue.cancel( queueContext );
361
+ isMounted.current = false;
362
+ };
363
+ }, [ registry, wrapSelect, depsChangedFlag ] );
364
+
365
+ if ( mapOutputError ) {
366
+ throw mapOutputError;
367
+ }
368
+
369
+ return mapOutput;
370
+ }