react-relay 18.0.0 → 18.2.0

Sign up to get free protection for your applications and to get access to all the features.
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;