react-relay 18.1.0 → 18.2.0

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