react-relay 18.1.0 → 18.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.
@@ -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;