react-relay 18.1.0 → 19.0.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.
Files changed (67) hide show
  1. package/ReactRelayContainerUtils.js.flow +2 -2
  2. package/ReactRelayContext.js +1 -1
  3. package/ReactRelayFragmentContainer.js.flow +5 -10
  4. package/ReactRelayLoggingContext.js.flow +21 -0
  5. package/ReactRelayPaginationContainer.js.flow +8 -8
  6. package/ReactRelayRefetchContainer.js.flow +8 -8
  7. package/ReactRelayTypes.js.flow +18 -11
  8. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +1 -8
  9. package/__flowtests__/ReactRelayPaginationContainer-flowtest.js.flow +2 -5
  10. package/__flowtests__/ReactRelayRefetchContainer-flowtest.js.flow +2 -5
  11. package/buildReactRelayContainer.js.flow +3 -3
  12. package/hooks.js +1 -1
  13. package/index.js +1 -1
  14. package/index.js.flow +3 -0
  15. package/legacy.js +1 -1
  16. package/lib/ReactRelayLoggingContext.js +6 -0
  17. package/lib/index.js +2 -0
  18. package/lib/relay-hooks/legacy/FragmentResource.js +4 -5
  19. package/lib/relay-hooks/legacy/useRefetchableFragmentNode.js +1 -1
  20. package/lib/relay-hooks/loadEntryPoint.js +3 -0
  21. package/lib/relay-hooks/loadQuery.js +9 -16
  22. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +7 -2
  23. package/lib/relay-hooks/readFragmentInternal.js +2 -2
  24. package/lib/relay-hooks/useFragmentInternal_CURRENT.js +12 -8
  25. package/lib/relay-hooks/useFragmentInternal_EXPERIMENTAL.js +12 -8
  26. package/lib/relay-hooks/useLoadMoreFunction.js +3 -2
  27. package/lib/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js +3 -2
  28. package/lib/relay-hooks/usePaginationFragment.js +5 -1
  29. package/lib/relay-hooks/usePrefetchableForwardPaginationFragment.js +228 -0
  30. package/lib/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js +227 -0
  31. package/lib/relay-hooks/useRefetchableFragmentInternal.js +3 -3
  32. package/lib/relay-hooks/useRelayLoggingContext.js +9 -0
  33. package/package.json +3 -3
  34. package/relay-hooks/EntryPointContainer.react.js.flow +13 -17
  35. package/relay-hooks/EntryPointTypes.flow.js.flow +7 -5
  36. package/relay-hooks/MatchContainer.js.flow +1 -1
  37. package/relay-hooks/legacy/FragmentResource.js.flow +5 -9
  38. package/relay-hooks/legacy/useBlockingPaginationFragment.js.flow +2 -17
  39. package/relay-hooks/legacy/useRefetchableFragmentNode.js.flow +1 -1
  40. package/relay-hooks/loadEntryPoint.js.flow +6 -0
  41. package/relay-hooks/loadQuery.js.flow +47 -39
  42. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +18 -3
  43. package/relay-hooks/readFragmentInternal.js.flow +2 -5
  44. package/relay-hooks/useEntryPointLoader.js.flow +4 -2
  45. package/relay-hooks/useFragment.js.flow +1 -0
  46. package/relay-hooks/useFragmentInternal.js.flow +2 -0
  47. package/relay-hooks/useFragmentInternal_CURRENT.js.flow +27 -12
  48. package/relay-hooks/useFragmentInternal_EXPERIMENTAL.js.flow +29 -12
  49. package/relay-hooks/useLazyLoadQuery.js.flow +4 -4
  50. package/relay-hooks/useLoadMoreFunction.js.flow +7 -7
  51. package/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js.flow +5 -7
  52. package/relay-hooks/usePaginationFragment.js.flow +6 -10
  53. package/relay-hooks/usePrefetchableForwardPaginationFragment.js.flow +436 -0
  54. package/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js.flow +435 -0
  55. package/relay-hooks/usePreloadedQuery.js.flow +1 -0
  56. package/relay-hooks/useQueryLoader.js.flow +5 -1
  57. package/relay-hooks/useQueryLoader_EXPERIMENTAL.js.flow +3 -1
  58. package/relay-hooks/useRefetchableFragment.js.flow +1 -0
  59. package/relay-hooks/useRefetchableFragmentInternal.js.flow +3 -3
  60. package/relay-hooks/useRelayLoggingContext.js.flow +21 -0
  61. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +1 -0
  62. package/react-relay-hooks.js +0 -4
  63. package/react-relay-hooks.min.js +0 -9
  64. package/react-relay-legacy.js +0 -4
  65. package/react-relay-legacy.min.js +0 -9
  66. package/react-relay.js +0 -4
  67. package/react-relay.min.js +0 -9
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow strict-local
8
+ * @format
9
+ * @oncall relay
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {RefetchFn} from './useRefetchableFragment';
15
+ import type {Options} from './useRefetchableFragmentInternal';
16
+ import type {FragmentType, Variables} from 'relay-runtime';
17
+ import type {PrefetchableRefetchableFragment} from 'relay-runtime';
18
+
19
+ const useFragment = require('./useFragment');
20
+ const useLoadMoreFunction = require('./useLoadMoreFunction');
21
+ const useRefetchableFragmentInternal = require('./useRefetchableFragmentInternal');
22
+ const useRelayEnvironment = require('./useRelayEnvironment');
23
+ const useStaticFragmentNodeWarning = require('./useStaticFragmentNodeWarning');
24
+ const invariant = require('invariant');
25
+ const {
26
+ useCallback,
27
+ useDebugValue,
28
+ useEffect,
29
+ useLayoutEffect,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ } = require('react');
34
+ const {
35
+ getFragment,
36
+ getFragmentIdentifier,
37
+ getPaginationMetadata,
38
+ } = require('relay-runtime');
39
+ const {
40
+ ConnectionInterface,
41
+ RelayFeatureFlags,
42
+ getSelector,
43
+ getValueAtPath,
44
+ } = require('relay-runtime');
45
+
46
+ type LoadMoreFn<TVariables: Variables> = (
47
+ count: number,
48
+ options?: {
49
+ onComplete?: (Error | null) => void,
50
+ UNSTABLE_extraVariables?: Partial<TVariables>,
51
+ },
52
+ ) => void;
53
+
54
+ export type ReturnType<TVariables, TData, TEdgeData, TKey> = {
55
+ // NOTE: This type ensures that the type of the returned data is either:
56
+ // - nullable if the provided ref type is nullable
57
+ // - non-nullable if the provided ref type is non-nullable
58
+ data: [+key: TKey] extends [+key: {+$fragmentSpreads: mixed, ...}]
59
+ ? TData
60
+ : ?TData,
61
+ loadNext: LoadMoreFn<TVariables>,
62
+ hasNext: boolean,
63
+ isLoadingNext: boolean,
64
+ refetch: RefetchFn<TVariables, TKey>,
65
+ edges: TEdgeData,
66
+ };
67
+
68
+ type LoadMoreOptions<TVariables> = {
69
+ UNSTABLE_extraVariables?: Partial<TVariables>,
70
+ onComplete?: (Error | null) => void,
71
+ };
72
+
73
+ export type GetExtraVariablesFn<TEdgeData, TData, TVariables, TKey> = ({
74
+ hasNext: boolean,
75
+ data: [+key: TKey] extends [+key: {+$fragmentSpreads: mixed, ...}]
76
+ ? TData
77
+ : ?TData,
78
+ getServerEdges: () => TEdgeData,
79
+ }) => Partial<TVariables>;
80
+
81
+ hook usePrefetchableForwardPaginationFragment<
82
+ TFragmentType: FragmentType,
83
+ TVariables: Variables,
84
+ TData,
85
+ TEdgeData,
86
+ TKey: ?{+$fragmentSpreads: TFragmentType, ...},
87
+ >(
88
+ fragmentInput: PrefetchableRefetchableFragment<
89
+ TFragmentType,
90
+ TData,
91
+ TEdgeData,
92
+ TVariables,
93
+ >,
94
+ parentFragmentRef: TKey,
95
+ bufferSize: number,
96
+ initialSize?: ?number,
97
+ prefetchingLoadMoreOptions?: {
98
+ UNSTABLE_extraVariables?:
99
+ | Partial<TVariables>
100
+ | GetExtraVariablesFn<TEdgeData, TData, TVariables, TKey>,
101
+ onComplete?: (Error | null) => void,
102
+ },
103
+ minimalFetchSize: number = 1,
104
+ disablePrefetching?: boolean = false,
105
+ ): ReturnType<TVariables, TData, TEdgeData, TKey> {
106
+ const fragmentNode = getFragment(fragmentInput);
107
+ useStaticFragmentNodeWarning(
108
+ fragmentNode,
109
+ 'first argument of usePrefetchableForwardPaginationFragment()',
110
+ );
111
+ const componentDisplayName = 'usePrefetchableForwardPaginationFragment()';
112
+
113
+ const {connectionPathInFragmentData, paginationRequest, paginationMetadata} =
114
+ getPaginationMetadata(fragmentNode, componentDisplayName);
115
+
116
+ const {fragmentData, fragmentRef, refetch} = useRefetchableFragmentInternal<
117
+ {variables: TVariables, response: TData},
118
+ {data?: TData},
119
+ >(fragmentNode, parentFragmentRef, componentDisplayName);
120
+ // TODO: Get rid of `getFragmentIdentifier`
121
+ const fragmentIdentifier = getFragmentIdentifier(fragmentNode, fragmentRef);
122
+
123
+ const edgeKeys = useMemo(() => {
124
+ const connection = getValueAtPath(
125
+ fragmentData,
126
+ connectionPathInFragmentData,
127
+ );
128
+ if (connection == null) {
129
+ return null;
130
+ }
131
+ const {EDGES} = ConnectionInterface.get();
132
+ // $FlowFixMe[incompatible-use]
133
+ return connection[EDGES];
134
+ }, [connectionPathInFragmentData, fragmentData]);
135
+
136
+ const sourceSize = edgeKeys == null ? -1 : edgeKeys.length;
137
+
138
+ const [_numInUse, setNumInUse] = useState(
139
+ initialSize != null ? initialSize : sourceSize,
140
+ );
141
+ let numInUse = _numInUse;
142
+ // We can only reset the source size when the component is
143
+ // updated with new edgeKeys
144
+ if (_numInUse === -1 && sourceSize !== -1) {
145
+ numInUse = initialSize != null ? initialSize : sourceSize;
146
+ setNumInUse(numInUse);
147
+ }
148
+
149
+ const environment = useRelayEnvironment();
150
+ const [isLoadingMore, reallySetIsLoadingMore] = useState(false);
151
+ const [isRefetching, setIsRefetching] = useState(false);
152
+ const availableSizeRef = useRef(0);
153
+ // Schedule this update since it must be observed by components at the same
154
+ // batch as when hasNext changes. hasNext is read from the store and store
155
+ // updates are scheduled, so this must be scheduled too.
156
+ const setIsLoadingMore = useCallback(
157
+ (value: boolean) => {
158
+ const schedule = environment.getScheduler()?.schedule;
159
+ if (schedule) {
160
+ schedule(() => {
161
+ reallySetIsLoadingMore(value);
162
+ });
163
+ } else {
164
+ reallySetIsLoadingMore(value);
165
+ }
166
+ },
167
+ [environment],
168
+ );
169
+
170
+ // `isLoadingMore` state is updated in a low priority, internally we need
171
+ // to synchronously get the loading state to decide whether to load more
172
+ const isLoadingMoreRef = useRef(false);
173
+
174
+ const observer = useMemo(() => {
175
+ function setIsLoadingFalse() {
176
+ isLoadingMoreRef.current = false;
177
+ setIsLoadingMore(false);
178
+ }
179
+ return {
180
+ start: () => {
181
+ isLoadingMoreRef.current = true;
182
+ // We want to make sure that `isLoadingMore` is updated immediately, to avoid
183
+ // product code triggering multiple `loadMore` calls
184
+ reallySetIsLoadingMore(true);
185
+ },
186
+ complete: setIsLoadingFalse,
187
+ error: setIsLoadingFalse,
188
+ unsubscribe: RelayFeatureFlags.ENABLE_USE_PAGINATION_IS_LOADING_FIX
189
+ ? setIsLoadingFalse
190
+ : undefined,
191
+ };
192
+ }, [setIsLoadingMore]);
193
+ const handleReset = useCallback(() => {
194
+ if (!isRefetching) {
195
+ // Do not reset items count during refetching
196
+ const schedule = environment.getScheduler()?.schedule;
197
+ if (schedule) {
198
+ schedule(() => {
199
+ setNumInUse(-1);
200
+ });
201
+ } else {
202
+ setNumInUse(-1);
203
+ }
204
+ }
205
+ isLoadingMoreRef.current = false;
206
+ setIsLoadingMore(false);
207
+ }, [environment, isRefetching, setIsLoadingMore]);
208
+
209
+ const [loadMore, hasNext, disposeFetchNext] = useLoadMoreFunction<TVariables>(
210
+ {
211
+ componentDisplayName,
212
+ connectionPathInFragmentData,
213
+ direction: 'forward',
214
+ fragmentData,
215
+ fragmentIdentifier,
216
+ fragmentNode,
217
+ fragmentRef,
218
+ paginationMetadata,
219
+ paginationRequest,
220
+ observer,
221
+ onReset: handleReset,
222
+ },
223
+ );
224
+
225
+ useLayoutEffect(() => {
226
+ // Make sure `availableSize` is updated before `showMore` from current render can be called
227
+ availableSizeRef.current = sourceSize - numInUse;
228
+ }, [numInUse, sourceSize]);
229
+
230
+ const prefetchingUNSTABLE_extraVariables =
231
+ prefetchingLoadMoreOptions?.UNSTABLE_extraVariables;
232
+ const prefetchingOnComplete = prefetchingLoadMoreOptions?.onComplete;
233
+
234
+ const showMore = useCallback(
235
+ (numToAdd: number, options?: LoadMoreOptions<TVariables>) => {
236
+ // Matches the behavior of `usePaginationFragment`. If there is a `loadMore` ongoing,
237
+ // the hook handles making the `loadMore` a no-op.
238
+ if (!isLoadingMoreRef.current || availableSizeRef.current >= 0) {
239
+ // Preemtively update `availableSizeRef`, so if two `loadMore` is called in the same tick,
240
+ // a second `loadMore` can be no-op
241
+ availableSizeRef.current -= numToAdd;
242
+
243
+ setNumInUse(lastNumInUse => {
244
+ return lastNumInUse + numToAdd;
245
+ });
246
+
247
+ // If the product needs more items from network, load the amount needed to fullfil
248
+ // the requirement and cache, capped at the current amount defined by product
249
+ if (!isLoadingMoreRef.current && availableSizeRef.current < 0) {
250
+ loadMore(
251
+ Math.max(
252
+ minimalFetchSize,
253
+ Math.min(numToAdd, bufferSize - availableSizeRef.current),
254
+ ),
255
+ // Keep options For backward compatibility
256
+ options ?? {
257
+ onComplete: prefetchingOnComplete,
258
+ UNSTABLE_extraVariables:
259
+ typeof prefetchingUNSTABLE_extraVariables === 'function'
260
+ ? // $FlowFixMe[incompatible-call]
261
+ prefetchingUNSTABLE_extraVariables({
262
+ hasNext,
263
+ // $FlowFixMe[incompatible-call]
264
+ data: fragmentData,
265
+ getServerEdges: () => {
266
+ const selector = getSelector(
267
+ // $FlowFixMe[incompatible-call]
268
+ edgesFragment,
269
+ edgeKeys,
270
+ );
271
+ if (selector == null) {
272
+ // $FlowFixMe[incompatible-call]
273
+ return [];
274
+ }
275
+ invariant(
276
+ selector.kind === 'PluralReaderSelector',
277
+ 'Expected a plural selector',
278
+ );
279
+ // $FlowFixMe[incompatible-call]
280
+ return selector.selectors.map(
281
+ sel => environment.lookup(sel).data,
282
+ );
283
+ },
284
+ })
285
+ : prefetchingUNSTABLE_extraVariables,
286
+ },
287
+ );
288
+ }
289
+ }
290
+ },
291
+ [
292
+ bufferSize,
293
+ loadMore,
294
+ minimalFetchSize,
295
+ edgeKeys,
296
+ fragmentData,
297
+ prefetchingUNSTABLE_extraVariables,
298
+ prefetchingOnComplete,
299
+ ],
300
+ );
301
+
302
+ const edgesFragment = fragmentInput.metadata?.refetch?.edgesFragment;
303
+ invariant(
304
+ edgesFragment != null,
305
+ 'usePrefetchableForwardPaginationFragment: Expected the edge fragment to be defined, ' +
306
+ 'please make sure you have added `prefetchable_pagination: true` to `@connection`',
307
+ );
308
+
309
+ // Always try to keep `bufferSize` items in the buffer
310
+ // Or load the number of items that have been registred to show
311
+ useEffect(() => {
312
+ if (
313
+ // Check the ref to avoid infinite `loadMore`, when a `loadMore` has started,
314
+ // but `isLoadingMore` isn't updated
315
+ !isLoadingMoreRef.current &&
316
+ // Check the original `isLoadingMore` so when `loadMore` is called, the internal
317
+ // `loadMore` hook has been updated with the latest cursor
318
+ !isLoadingMore &&
319
+ !isRefetching &&
320
+ !disablePrefetching &&
321
+ hasNext &&
322
+ (sourceSize - numInUse < bufferSize || numInUse > sourceSize)
323
+ ) {
324
+ const onComplete = prefetchingOnComplete;
325
+ loadMore(
326
+ Math.max(
327
+ bufferSize - Math.max(sourceSize - numInUse, 0),
328
+ numInUse - sourceSize,
329
+ minimalFetchSize,
330
+ ),
331
+ {
332
+ onComplete,
333
+ UNSTABLE_extraVariables:
334
+ typeof prefetchingUNSTABLE_extraVariables === 'function'
335
+ ? // $FlowFixMe[incompatible-call]
336
+ prefetchingUNSTABLE_extraVariables({
337
+ hasNext,
338
+ // $FlowFixMe[incompatible-call]
339
+ data: fragmentData,
340
+ getServerEdges: () => {
341
+ const selector = getSelector(edgesFragment, edgeKeys);
342
+ if (selector == null) {
343
+ // $FlowFixMe[incompatible-call]
344
+ return [];
345
+ }
346
+ invariant(
347
+ selector.kind === 'PluralReaderSelector',
348
+ 'Expected a plural selector',
349
+ );
350
+ // $FlowFixMe[incompatible-call]
351
+ return selector.selectors.map(
352
+ sel => environment.lookup(sel).data,
353
+ );
354
+ },
355
+ })
356
+ : prefetchingUNSTABLE_extraVariables,
357
+ },
358
+ );
359
+ }
360
+ }, [
361
+ hasNext,
362
+ bufferSize,
363
+ isRefetching,
364
+ loadMore,
365
+ numInUse,
366
+ prefetchingUNSTABLE_extraVariables,
367
+ prefetchingOnComplete,
368
+ sourceSize,
369
+ edgeKeys,
370
+ isLoadingMore,
371
+ minimalFetchSize,
372
+ environment,
373
+ edgesFragment,
374
+ ]);
375
+
376
+ const realNumInUse = Math.min(numInUse, sourceSize);
377
+
378
+ const derivedEdgeKeys: $ReadOnlyArray<mixed> = useMemo(
379
+ () => edgeKeys?.slice(0, realNumInUse) ?? [],
380
+ [edgeKeys, realNumInUse],
381
+ );
382
+
383
+ // $FlowExpectedError[incompatible-call] - we know derivedEdgeKeys are the correct keys
384
+ const edges: TEdgeData = useFragment(edgesFragment, derivedEdgeKeys);
385
+
386
+ const refetchPagination = useCallback(
387
+ (variables: TVariables, options?: Options) => {
388
+ disposeFetchNext();
389
+ setIsRefetching(true);
390
+ return refetch(variables, {
391
+ ...options,
392
+ onComplete: maybeError => {
393
+ // Need to be batched with the store update
394
+ const schedule = environment.getScheduler()?.schedule;
395
+ if (schedule) {
396
+ schedule(() => {
397
+ setIsRefetching(false);
398
+ setNumInUse(-1);
399
+ });
400
+ } else {
401
+ setIsRefetching(false);
402
+ setNumInUse(-1);
403
+ }
404
+ options?.onComplete?.(maybeError);
405
+ },
406
+ __environment: undefined,
407
+ });
408
+ },
409
+ [disposeFetchNext, environment, refetch],
410
+ );
411
+
412
+ if (__DEV__) {
413
+ // $FlowFixMe[react-rule-hook]
414
+ // $FlowFixMe[react-rule-hook-conditional]
415
+ useDebugValue({
416
+ fragment: fragmentNode.name,
417
+ data: fragmentData,
418
+ hasNext,
419
+ isLoadingNext: isLoadingMore,
420
+ });
421
+ }
422
+
423
+ return {
424
+ edges,
425
+ // $FlowFixMe[incompatible-return]
426
+ data: fragmentData,
427
+ loadNext: showMore,
428
+ hasNext: hasNext || sourceSize > numInUse,
429
+ // Only reflect `isLoadingMore` if the product depends on it, do not refelect
430
+ // `isLoaindgMore` state if it is for fufilling the buffer
431
+ isLoadingNext: isLoadingMore && numInUse > sourceSize,
432
+ refetch: refetchPagination,
433
+ };
434
+ }
435
+
436
+ module.exports = usePrefetchableForwardPaginationFragment;