react-relay 18.0.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.
Files changed (41) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +2 -5
  3. package/buildReactRelayContainer.js.flow +1 -0
  4. package/hooks.js +1 -1
  5. package/index.js +1 -1
  6. package/index.js.flow +3 -0
  7. package/legacy.js +1 -1
  8. package/lib/index.js +2 -0
  9. package/lib/relay-hooks/getConnectionState.js +47 -0
  10. package/lib/relay-hooks/legacy/FragmentResource.js +3 -8
  11. package/lib/relay-hooks/loadQuery.js +5 -14
  12. package/lib/relay-hooks/readFragmentInternal.js +2 -4
  13. package/lib/relay-hooks/useFragmentInternal.js +1 -1
  14. package/lib/relay-hooks/useFragmentInternal_CURRENT.js +3 -10
  15. package/lib/relay-hooks/useFragmentInternal_EXPERIMENTAL.js +6 -28
  16. package/lib/relay-hooks/useLoadMoreFunction.js +10 -43
  17. package/lib/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js +130 -0
  18. package/lib/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js +227 -0
  19. package/lib/relay-hooks/useQueryLoader.js +8 -0
  20. package/lib/relay-hooks/useQueryLoader_EXPERIMENTAL.js +120 -0
  21. package/package.json +2 -2
  22. package/react-relay-hooks.js +2 -2
  23. package/react-relay-hooks.min.js +2 -2
  24. package/react-relay-legacy.js +1 -1
  25. package/react-relay-legacy.min.js +1 -1
  26. package/react-relay.js +2 -2
  27. package/react-relay.min.js +2 -2
  28. package/relay-hooks/EntryPointTypes.flow.js.flow +2 -2
  29. package/relay-hooks/MatchContainer.js.flow +1 -1
  30. package/relay-hooks/getConnectionState.js.flow +97 -0
  31. package/relay-hooks/legacy/FragmentResource.js.flow +4 -16
  32. package/relay-hooks/loadQuery.js.flow +30 -38
  33. package/relay-hooks/readFragmentInternal.js.flow +1 -10
  34. package/relay-hooks/useFragmentInternal.js.flow +1 -1
  35. package/relay-hooks/useFragmentInternal_CURRENT.js.flow +7 -22
  36. package/relay-hooks/useFragmentInternal_EXPERIMENTAL.js.flow +8 -56
  37. package/relay-hooks/useLoadMoreFunction.js.flow +14 -80
  38. package/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js.flow +280 -0
  39. package/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js.flow +433 -0
  40. package/relay-hooks/useQueryLoader.js.flow +27 -3
  41. package/relay-hooks/useQueryLoader_EXPERIMENTAL.js.flow +253 -0
@@ -0,0 +1,280 @@
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 {
15
+ ConcreteRequest,
16
+ Direction,
17
+ Disposable,
18
+ GraphQLResponse,
19
+ Observer,
20
+ ReaderFragment,
21
+ ReaderPaginationMetadata,
22
+ Subscription,
23
+ Variables,
24
+ } from 'relay-runtime';
25
+
26
+ const getConnectionState = require('./getConnectionState');
27
+ const useIsMountedRef = require('./useIsMountedRef');
28
+ const useIsOperationNodeActive = require('./useIsOperationNodeActive');
29
+ const useRelayEnvironment = require('./useRelayEnvironment');
30
+ const invariant = require('invariant');
31
+ const {useCallback, useRef, useState} = require('react');
32
+ const {
33
+ __internal: {fetchQuery},
34
+ createOperationDescriptor,
35
+ getPaginationVariables,
36
+ getRefetchMetadata,
37
+ getSelector,
38
+ } = require('relay-runtime');
39
+ const warning = require('warning');
40
+
41
+ export type LoadMoreFn<TVariables: Variables> = (
42
+ count: number,
43
+ options?: {
44
+ onComplete?: (Error | null) => void,
45
+ UNSTABLE_extraVariables?: Partial<TVariables>,
46
+ },
47
+ ) => Disposable;
48
+
49
+ export type UseLoadMoreFunctionArgs = {
50
+ direction: Direction,
51
+ fragmentNode: ReaderFragment,
52
+ fragmentRef: mixed,
53
+ fragmentIdentifier: string,
54
+ fragmentData: mixed,
55
+ connectionPathInFragmentData: $ReadOnlyArray<string | number>,
56
+ paginationRequest: ConcreteRequest,
57
+ paginationMetadata: ReaderPaginationMetadata,
58
+ componentDisplayName: string,
59
+ observer: Observer<GraphQLResponse>,
60
+ onReset: () => void,
61
+ };
62
+
63
+ hook useLoadMoreFunction_EXPERIMENTAL<TVariables: Variables>(
64
+ args: UseLoadMoreFunctionArgs,
65
+ ): [
66
+ // Function to load more data
67
+ LoadMoreFn<TVariables>,
68
+ // Whether the connection has more data to load
69
+ boolean,
70
+ // Force dispose function which cancels the in-flight fetch itself, and callbacks
71
+ () => void,
72
+ ] {
73
+ const {
74
+ direction,
75
+ fragmentNode,
76
+ fragmentRef,
77
+ fragmentIdentifier,
78
+ fragmentData,
79
+ connectionPathInFragmentData,
80
+ paginationRequest,
81
+ paginationMetadata,
82
+ componentDisplayName,
83
+ observer,
84
+ onReset,
85
+ } = args;
86
+ const environment = useRelayEnvironment();
87
+
88
+ const {identifierInfo} = getRefetchMetadata(
89
+ fragmentNode,
90
+ componentDisplayName,
91
+ );
92
+ const identifierValue =
93
+ identifierInfo?.identifierField != null &&
94
+ fragmentData != null &&
95
+ typeof fragmentData === 'object'
96
+ ? fragmentData[identifierInfo.identifierField]
97
+ : null;
98
+
99
+ const fetchStatusRef = useRef<
100
+ {kind: 'fetching', subscription: Subscription} | {kind: 'none'},
101
+ >({kind: 'none'});
102
+ const [mirroredEnvironment, setMirroredEnvironment] = useState(environment);
103
+ const [mirroredFragmentIdentifier, setMirroredFragmentIdentifier] =
104
+ useState(fragmentIdentifier);
105
+
106
+ const isParentQueryActive = useIsOperationNodeActive(
107
+ fragmentNode,
108
+ fragmentRef,
109
+ );
110
+
111
+ const forceDisposeFn = useCallback(() => {
112
+ // $FlowFixMe[react-rule-unsafe-ref]
113
+ if (fetchStatusRef.current.kind === 'fetching') {
114
+ // $FlowFixMe[react-rule-unsafe-ref]
115
+ fetchStatusRef.current.subscription.unsubscribe();
116
+ }
117
+ // $FlowFixMe[react-rule-unsafe-ref]
118
+ fetchStatusRef.current = {kind: 'none'};
119
+ }, []);
120
+
121
+ const shouldReset =
122
+ environment !== mirroredEnvironment ||
123
+ fragmentIdentifier !== mirroredFragmentIdentifier;
124
+ if (shouldReset) {
125
+ forceDisposeFn();
126
+ onReset();
127
+ setMirroredEnvironment(environment);
128
+ setMirroredFragmentIdentifier(fragmentIdentifier);
129
+ }
130
+
131
+ const {cursor, hasMore} = getConnectionState(
132
+ direction,
133
+ fragmentNode,
134
+ fragmentData,
135
+ connectionPathInFragmentData,
136
+ );
137
+
138
+ const isMountedRef = useIsMountedRef();
139
+ const loadMore = useCallback(
140
+ (
141
+ count: number,
142
+ options: void | {
143
+ UNSTABLE_extraVariables?: Partial<TVariables>,
144
+ onComplete?: (Error | null) => void,
145
+ },
146
+ ) => {
147
+ // TODO(T41131846): Fetch/Caching policies for loadMore
148
+
149
+ const onComplete = options?.onComplete;
150
+ if (isMountedRef.current !== true) {
151
+ // Bail out and warn if we're trying to paginate after the component
152
+ // has unmounted
153
+ warning(
154
+ false,
155
+ 'Relay: Unexpected fetch on unmounted component for fragment ' +
156
+ '`%s` in `%s`. It looks like some instances of your component are ' +
157
+ 'still trying to fetch data but they already unmounted. ' +
158
+ 'Please make sure you clear all timers, intervals, ' +
159
+ 'async calls, etc that may trigger a fetch.',
160
+ fragmentNode.name,
161
+ componentDisplayName,
162
+ );
163
+ return {dispose: () => {}};
164
+ }
165
+
166
+ const fragmentSelector = getSelector(fragmentNode, fragmentRef);
167
+ if (
168
+ fetchStatusRef.current.kind === 'fetching' ||
169
+ fragmentData == null ||
170
+ isParentQueryActive
171
+ ) {
172
+ if (fragmentSelector == null) {
173
+ warning(
174
+ false,
175
+ 'Relay: Unexpected fetch while using a null fragment ref ' +
176
+ 'for fragment `%s` in `%s`. When fetching more items, we expect ' +
177
+ "initial fragment data to be non-null. Please make sure you're " +
178
+ 'passing a valid fragment ref to `%s` before paginating.',
179
+ fragmentNode.name,
180
+ componentDisplayName,
181
+ componentDisplayName,
182
+ );
183
+ }
184
+
185
+ if (onComplete) {
186
+ onComplete(null);
187
+ }
188
+ return {dispose: () => {}};
189
+ }
190
+
191
+ invariant(
192
+ fragmentSelector != null &&
193
+ fragmentSelector.kind !== 'PluralReaderSelector',
194
+ 'Relay: Expected to be able to find a non-plural fragment owner for ' +
195
+ "fragment `%s` when using `%s`. If you're seeing this, " +
196
+ 'this is likely a bug in Relay.',
197
+ fragmentNode.name,
198
+ componentDisplayName,
199
+ );
200
+
201
+ const parentVariables = fragmentSelector.owner.variables;
202
+ const fragmentVariables = fragmentSelector.variables;
203
+ const extraVariables = options?.UNSTABLE_extraVariables;
204
+ const baseVariables = {
205
+ ...parentVariables,
206
+ ...fragmentVariables,
207
+ };
208
+ const paginationVariables = getPaginationVariables(
209
+ direction,
210
+ count,
211
+ cursor,
212
+ baseVariables,
213
+ {...extraVariables},
214
+ paginationMetadata,
215
+ );
216
+
217
+ // If the query needs an identifier value ('id' or similar) and one
218
+ // was not explicitly provided, read it from the fragment data.
219
+ if (identifierInfo != null) {
220
+ // @refetchable fragments are guaranteed to have an `id` selection
221
+ // if the type is Node, implements Node, or is @fetchable. Double-check
222
+ // that there actually is a value at runtime.
223
+ if (typeof identifierValue !== 'string') {
224
+ warning(
225
+ false,
226
+ 'Relay: Expected result to have a string ' +
227
+ '`%s` in order to refetch, got `%s`.',
228
+ identifierInfo.identifierField,
229
+ identifierValue,
230
+ );
231
+ }
232
+ paginationVariables[identifierInfo.identifierQueryVariableName] =
233
+ identifierValue;
234
+ }
235
+
236
+ const paginationQuery = createOperationDescriptor(
237
+ paginationRequest,
238
+ paginationVariables,
239
+ {force: true},
240
+ );
241
+ fetchQuery(environment, paginationQuery).subscribe({
242
+ ...observer,
243
+ start: subscription => {
244
+ fetchStatusRef.current = {kind: 'fetching', subscription};
245
+ observer.start && observer.start(subscription);
246
+ },
247
+ complete: () => {
248
+ fetchStatusRef.current = {kind: 'none'};
249
+ observer.complete && observer.complete();
250
+ onComplete && onComplete(null);
251
+ },
252
+ error: error => {
253
+ fetchStatusRef.current = {kind: 'none'};
254
+ observer.complete && observer.complete();
255
+ onComplete && onComplete(error);
256
+ },
257
+ });
258
+ return {
259
+ dispose: () => {},
260
+ };
261
+ },
262
+ // NOTE: We disable react-hooks-deps warning because all values
263
+ // inside paginationMetadata are static
264
+ // eslint-disable-next-line react-hooks/exhaustive-deps
265
+ [
266
+ environment,
267
+ identifierValue,
268
+ direction,
269
+ cursor,
270
+ isParentQueryActive,
271
+ fragmentData,
272
+ fragmentNode.name,
273
+ fragmentRef,
274
+ componentDisplayName,
275
+ ],
276
+ );
277
+ return [loadMore, hasMore, forceDisposeFn];
278
+ }
279
+
280
+ module.exports = useLoadMoreFunction_EXPERIMENTAL;
@@ -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;