react-relay 14.0.0 → 15.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 (194) hide show
  1. package/ReactRelayContainerUtils.js.flow +1 -2
  2. package/ReactRelayContext.js +1 -1
  3. package/ReactRelayContext.js.flow +1 -2
  4. package/ReactRelayFragmentContainer.js.flow +6 -4
  5. package/ReactRelayFragmentMockRenderer.js.flow +1 -2
  6. package/ReactRelayLocalQueryRenderer.js.flow +5 -5
  7. package/ReactRelayPaginationContainer.js.flow +21 -14
  8. package/ReactRelayQueryFetcher.js.flow +28 -16
  9. package/ReactRelayQueryRenderer.js.flow +42 -13
  10. package/ReactRelayQueryRendererContext.js.flow +2 -3
  11. package/ReactRelayRefetchContainer.js.flow +9 -9
  12. package/ReactRelayTestMocker.js.flow +3 -3
  13. package/ReactRelayTypes.js.flow +7 -8
  14. package/RelayContext.js.flow +1 -2
  15. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +4 -5
  16. package/__flowtests__/ReactRelayPaginationContainer-flowtest.js.flow +4 -5
  17. package/__flowtests__/ReactRelayRefetchContainer-flowtest.js.flow +4 -5
  18. package/__flowtests__/RelayModern-flowtest.js.flow +3 -4
  19. package/__flowtests__/RelayModernFlowtest_badref.graphql.js.flow +3 -4
  20. package/__flowtests__/RelayModernFlowtest_notref.graphql.js.flow +3 -4
  21. package/__flowtests__/RelayModernFlowtest_user.graphql.js.flow +3 -4
  22. package/__flowtests__/RelayModernFlowtest_users.graphql.js.flow +3 -4
  23. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +3 -1
  24. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +3 -1
  25. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +4 -2
  26. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -1
  27. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +4 -2
  28. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -1
  29. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +4 -2
  30. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +4 -2
  31. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +3 -1
  32. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +3 -1
  33. package/assertFragmentMap.js.flow +1 -2
  34. package/buildReactRelayContainer.js.flow +7 -7
  35. package/getRootVariablesForFragments.js.flow +1 -3
  36. package/hooks.js +1 -1
  37. package/hooks.js.flow +4 -2
  38. package/index.js +1 -1
  39. package/index.js.flow +6 -2
  40. package/isRelayEnvironment.js.flow +1 -2
  41. package/jest-react/enqueueTask.js.flow +1 -1
  42. package/jest-react/index.js.flow +1 -1
  43. package/jest-react/internalAct.js.flow +1 -1
  44. package/legacy.js +1 -1
  45. package/legacy.js.flow +1 -2
  46. package/lib/ReactRelayContainerUtils.js +2 -3
  47. package/lib/ReactRelayContext.js +3 -4
  48. package/lib/ReactRelayFragmentContainer.js +47 -73
  49. package/lib/ReactRelayFragmentMockRenderer.js +2 -4
  50. package/lib/ReactRelayLocalQueryRenderer.js +18 -31
  51. package/lib/ReactRelayPaginationContainer.js +74 -164
  52. package/lib/ReactRelayQueryFetcher.js +49 -76
  53. package/lib/ReactRelayQueryRenderer.js +63 -84
  54. package/lib/ReactRelayQueryRendererContext.js +2 -2
  55. package/lib/ReactRelayRefetchContainer.js +58 -108
  56. package/lib/ReactRelayTestMocker.js +33 -68
  57. package/lib/ReactRelayTypes.js +2 -1
  58. package/lib/RelayContext.js +4 -7
  59. package/lib/assertFragmentMap.js +3 -5
  60. package/lib/buildReactRelayContainer.js +11 -27
  61. package/lib/getRootVariablesForFragments.js +6 -10
  62. package/lib/hooks.js +5 -18
  63. package/lib/index.js +7 -24
  64. package/lib/isRelayEnvironment.js +5 -4
  65. package/lib/jest-react/enqueueTask.js +5 -9
  66. package/lib/jest-react/index.js +0 -1
  67. package/lib/jest-react/internalAct.js +9 -20
  68. package/lib/legacy.js +2 -8
  69. package/lib/multi-actor/ActorChange.js +2 -5
  70. package/lib/multi-actor/index.js +2 -1
  71. package/lib/multi-actor/useRelayActorEnvironment.js +4 -8
  72. package/lib/relay-hooks/EntryPointContainer.react.js +9 -15
  73. package/lib/relay-hooks/EntryPointTypes.flow.js +5 -3
  74. package/lib/relay-hooks/FragmentResource.js +109 -203
  75. package/lib/relay-hooks/HooksImplementation.js +3 -6
  76. package/lib/relay-hooks/InternalLogger.js +2 -3
  77. package/lib/relay-hooks/LRUCache.js +2 -20
  78. package/lib/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js +33 -54
  79. package/lib/relay-hooks/MatchContainer.js +15 -24
  80. package/lib/relay-hooks/ProfilerContext.js +3 -3
  81. package/lib/relay-hooks/QueryResource.js +31 -101
  82. package/lib/relay-hooks/RelayEnvironmentProvider.js +5 -9
  83. package/lib/relay-hooks/SuspenseResource.js +9 -33
  84. package/lib/relay-hooks/loadEntryPoint.js +19 -31
  85. package/lib/relay-hooks/loadQuery.js +42 -78
  86. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +11 -37
  87. package/lib/relay-hooks/prepareEntryPoint_DEPRECATED.js +9 -15
  88. package/lib/relay-hooks/react-cache/RelayReactCache.js +7 -12
  89. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +27 -81
  90. package/lib/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js +206 -0
  91. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +195 -215
  92. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +5 -15
  93. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +17 -24
  94. package/lib/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js +149 -0
  95. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +24 -39
  96. package/lib/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js +325 -0
  97. package/lib/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js +37 -0
  98. package/lib/relay-hooks/useBlockingPaginationFragment.js +73 -93
  99. package/lib/relay-hooks/useClientQuery.js +30 -0
  100. package/lib/relay-hooks/useEntryPointLoader.js +18 -38
  101. package/lib/relay-hooks/useFetchTrackingRef.js +10 -12
  102. package/lib/relay-hooks/useFragment.js +8 -19
  103. package/lib/relay-hooks/useFragmentNode.js +20 -32
  104. package/lib/relay-hooks/useIsMountedRef.js +4 -6
  105. package/lib/relay-hooks/useIsOperationNodeActive.js +8 -20
  106. package/lib/relay-hooks/useIsParentQueryActive.js +3 -6
  107. package/lib/relay-hooks/useLazyLoadQuery.js +7 -24
  108. package/lib/relay-hooks/useLazyLoadQueryNode.js +23 -34
  109. package/lib/relay-hooks/useLoadMoreFunction.js +46 -78
  110. package/lib/relay-hooks/useMemoOperationDescriptor.js +6 -15
  111. package/lib/relay-hooks/useMemoVariables.js +15 -34
  112. package/lib/relay-hooks/useMutation.js +9 -27
  113. package/lib/relay-hooks/usePaginationFragment.js +73 -76
  114. package/lib/relay-hooks/usePreloadedQuery.js +13 -44
  115. package/lib/relay-hooks/useQueryLoader.js +24 -49
  116. package/lib/relay-hooks/useRefetchableFragment.js +19 -17
  117. package/lib/relay-hooks/useRefetchableFragmentNode.js +65 -109
  118. package/lib/relay-hooks/useRelayEnvironment.js +4 -8
  119. package/lib/relay-hooks/useStaticFragmentNodeWarning.js +4 -8
  120. package/lib/relay-hooks/useSubscribeToInvalidationState.js +8 -9
  121. package/lib/relay-hooks/useSubscription.js +5 -10
  122. package/lib/relay-hooks/useUnsafeRef_DEPRECATED.js +29 -0
  123. package/multi-actor/ActorChange.js.flow +1 -1
  124. package/multi-actor/index.js.flow +1 -1
  125. package/multi-actor/useRelayActorEnvironment.js.flow +2 -4
  126. package/package.json +2 -2
  127. package/react-relay-hooks.js +2 -2
  128. package/react-relay-hooks.min.js +2 -2
  129. package/react-relay-legacy.js +2 -2
  130. package/react-relay-legacy.min.js +2 -2
  131. package/react-relay.js +2 -2
  132. package/react-relay.min.js +2 -2
  133. package/relay-hooks/EntryPointContainer.react.js.flow +3 -5
  134. package/relay-hooks/EntryPointTypes.flow.js.flow +37 -37
  135. package/relay-hooks/FragmentResource.js.flow +43 -19
  136. package/relay-hooks/HooksImplementation.js.flow +7 -9
  137. package/relay-hooks/InternalLogger.js.flow +1 -3
  138. package/relay-hooks/LRUCache.js.flow +1 -3
  139. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +19 -14
  140. package/relay-hooks/MatchContainer.js.flow +6 -8
  141. package/relay-hooks/ProfilerContext.js.flow +1 -3
  142. package/relay-hooks/QueryResource.js.flow +29 -11
  143. package/relay-hooks/RelayEnvironmentProvider.js.flow +4 -6
  144. package/relay-hooks/SuspenseResource.js.flow +1 -3
  145. package/relay-hooks/__flowtests__/EntryPointTypes/EntryPointElementConfig-flowtest.js.flow +6 -4
  146. package/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js.flow +4 -4
  147. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +3 -1
  148. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +3 -1
  149. package/relay-hooks/__flowtests__/useBlockingPaginationFragment-flowtest.js.flow +39 -39
  150. package/relay-hooks/__flowtests__/useFragment-flowtest.js.flow +1 -3
  151. package/relay-hooks/__flowtests__/usePaginationFragment-flowtest.js.flow +37 -38
  152. package/relay-hooks/__flowtests__/useRefetchableFragment-flowtest.js.flow +18 -20
  153. package/relay-hooks/__flowtests__/utils.js.flow +21 -12
  154. package/relay-hooks/loadEntryPoint.js.flow +11 -6
  155. package/relay-hooks/loadQuery.js.flow +11 -7
  156. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +9 -12
  157. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +13 -10
  158. package/relay-hooks/react-cache/RelayReactCache.js.flow +1 -3
  159. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +26 -20
  160. package/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js.flow +297 -0
  161. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +136 -96
  162. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +1 -3
  163. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +3 -5
  164. package/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js.flow +190 -0
  165. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +3 -6
  166. package/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js.flow +601 -0
  167. package/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js.flow +65 -0
  168. package/relay-hooks/useBlockingPaginationFragment.js.flow +86 -59
  169. package/relay-hooks/useClientQuery.js.flow +39 -0
  170. package/relay-hooks/useEntryPointLoader.js.flow +16 -14
  171. package/relay-hooks/useFetchTrackingRef.js.flow +7 -8
  172. package/relay-hooks/useFragment.js.flow +2 -4
  173. package/relay-hooks/useFragmentNode.js.flow +7 -8
  174. package/relay-hooks/useIsMountedRef.js.flow +2 -4
  175. package/relay-hooks/useIsOperationNodeActive.js.flow +1 -1
  176. package/relay-hooks/useIsParentQueryActive.js.flow +1 -1
  177. package/relay-hooks/useLazyLoadQuery.js.flow +9 -32
  178. package/relay-hooks/useLazyLoadQueryNode.js.flow +4 -6
  179. package/relay-hooks/useLoadMoreFunction.js.flow +20 -17
  180. package/relay-hooks/useMemoOperationDescriptor.js.flow +3 -5
  181. package/relay-hooks/useMemoVariables.js.flow +13 -31
  182. package/relay-hooks/useMutation.js.flow +6 -8
  183. package/relay-hooks/usePaginationFragment.js.flow +75 -43
  184. package/relay-hooks/usePreloadedQuery.js.flow +49 -43
  185. package/relay-hooks/useQueryLoader.js.flow +89 -28
  186. package/relay-hooks/useRefetchableFragment.js.flow +83 -23
  187. package/relay-hooks/useRefetchableFragmentNode.js.flow +26 -22
  188. package/relay-hooks/useRelayEnvironment.js.flow +2 -4
  189. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +3 -5
  190. package/relay-hooks/useSubscribeToInvalidationState.js.flow +2 -4
  191. package/relay-hooks/useSubscription.js.flow +1 -3
  192. package/relay-hooks/useUnsafeRef_DEPRECATED.js.flow +25 -0
  193. package/lib/readContext.js +0 -28
  194. package/readContext.js.flow +0 -31
@@ -5,14 +5,13 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  * @flow strict-local
8
- * @emails oncall+relay
9
8
  * @format
9
+ * @oncall relay
10
10
  */
11
11
 
12
- // flowlint ambiguous-object-type:error
13
-
14
12
  'use strict';
15
13
 
14
+ import type {QueryResult} from '../QueryResource';
16
15
  import type {
17
16
  CacheConfig,
18
17
  FetchPolicy,
@@ -22,13 +21,17 @@ import type {
22
21
  SelectorData,
23
22
  Snapshot,
24
23
  } from 'relay-runtime';
25
- import type {MissingClientEdgeRequestInfo} from 'relay-runtime/store/RelayStoreTypes';
24
+ import type {
25
+ MissingClientEdgeRequestInfo,
26
+ MissingLiveResolverField,
27
+ } from 'relay-runtime/store/RelayStoreTypes';
26
28
 
29
+ const {getQueryResourceForEnvironment} = require('../QueryResource');
27
30
  const useRelayEnvironment = require('../useRelayEnvironment');
28
- const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
29
31
  const invariant = require('invariant');
30
32
  const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
31
33
  const {
34
+ __internal: {fetchQuery: fetchQueryInternal},
32
35
  areEqualSelectors,
33
36
  createOperationDescriptor,
34
37
  getPendingOperationsForFragment,
@@ -39,18 +42,17 @@ const {
39
42
  } = require('relay-runtime');
40
43
  const warning = require('warning');
41
44
 
42
- type FragmentQueryOptions = {|
45
+ type FragmentQueryOptions = {
43
46
  fetchPolicy?: FetchPolicy,
44
47
  networkCacheConfig?: ?CacheConfig,
45
- |};
48
+ };
46
49
 
47
50
  type FragmentState = $ReadOnly<
48
- | {|kind: 'bailout'|}
49
- | {|kind: 'singular', snapshot: Snapshot, epoch: number|}
50
- | {|kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number|},
51
+ | {kind: 'bailout'}
52
+ | {kind: 'singular', snapshot: Snapshot, epoch: number}
53
+ | {kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number},
51
54
  >;
52
55
 
53
- type StateUpdater<T> = (T | (T => T)) => void;
54
56
  type StateUpdaterFunction<T> = ((T) => T) => void;
55
57
 
56
58
  function isMissingData(state: FragmentState): boolean {
@@ -71,7 +73,7 @@ function getMissingClientEdges(
71
73
  } else if (state.kind === 'singular') {
72
74
  return state.snapshot.missingClientEdges ?? null;
73
75
  } else {
74
- let edges = null;
76
+ let edges: null | Array<MissingClientEdgeRequestInfo> = null;
75
77
  for (const snapshot of state.snapshots) {
76
78
  if (snapshot.missingClientEdges) {
77
79
  edges = edges ?? [];
@@ -84,6 +86,27 @@ function getMissingClientEdges(
84
86
  }
85
87
  }
86
88
 
89
+ function getSuspendingLiveResolver(
90
+ state: FragmentState,
91
+ ): $ReadOnlyArray<MissingLiveResolverField> | null {
92
+ if (state.kind === 'bailout') {
93
+ return null;
94
+ } else if (state.kind === 'singular') {
95
+ return state.snapshot.missingLiveResolverFields ?? null;
96
+ } else {
97
+ let missingFields: null | Array<MissingLiveResolverField> = null;
98
+ for (const snapshot of state.snapshots) {
99
+ if (snapshot.missingLiveResolverFields) {
100
+ missingFields = missingFields ?? [];
101
+ for (const edge of snapshot.missingLiveResolverFields) {
102
+ missingFields.push(edge);
103
+ }
104
+ }
105
+ }
106
+ return missingFields;
107
+ }
108
+ }
109
+
87
110
  function handlePotentialSnapshotErrorsForState(
88
111
  environment: IEnvironment,
89
112
  state: FragmentState,
@@ -190,7 +213,7 @@ function handleMissingClientEdge(
190
213
  parentFragmentRef: mixed,
191
214
  missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
192
215
  queryOptions?: FragmentQueryOptions,
193
- ): () => () => void {
216
+ ): QueryResult {
194
217
  const originalVariables = getVariablesFromFragment(
195
218
  parentFragmentNode,
196
219
  parentFragmentRef,
@@ -206,16 +229,14 @@ function handleMissingClientEdge(
206
229
  );
207
230
  // This may suspend. We don't need to do anything with the results; all we're
208
231
  // doing here is started the query if needed and retaining and releasing it
209
- // according to the component mount/suspense cycle; getQueryResultOrFetchQuery
232
+ // according to the component mount/suspense cycle; QueryResource
210
233
  // already handles this by itself.
211
- const [_, effect] = getQueryResultOrFetchQuery(
212
- environment,
234
+ const QueryResource = getQueryResourceForEnvironment(environment);
235
+ return QueryResource.prepare(
213
236
  queryOperationDescriptor,
214
- {
215
- fetchPolicy: queryOptions?.fetchPolicy,
216
- },
237
+ fetchQueryInternal(environment, queryOperationDescriptor),
238
+ queryOptions?.fetchPolicy,
217
239
  );
218
- return effect;
219
240
  }
220
241
 
221
242
  function subscribeToSnapshot(
@@ -227,11 +248,22 @@ function subscribeToSnapshot(
227
248
  return () => {};
228
249
  } else if (state.kind === 'singular') {
229
250
  const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
230
- setState(_ => ({
231
- kind: 'singular',
232
- snapshot: latestSnapshot,
233
- epoch: environment.getStore().getEpoch(),
234
- }));
251
+ setState(prevState => {
252
+ // In theory a setState from a subscription could be batched together
253
+ // with a setState to change the fragment selector. Guard against this
254
+ // by bailing out of the state update if the selector has changed.
255
+ if (
256
+ prevState.kind !== 'singular' ||
257
+ prevState.snapshot.selector !== latestSnapshot.selector
258
+ ) {
259
+ return prevState;
260
+ }
261
+ return {
262
+ kind: 'singular',
263
+ snapshot: latestSnapshot,
264
+ epoch: environment.getStore().getEpoch(),
265
+ };
266
+ });
235
267
  });
236
268
  return () => {
237
269
  disposable.dispose();
@@ -239,12 +271,17 @@ function subscribeToSnapshot(
239
271
  } else {
240
272
  const disposables = state.snapshots.map((snapshot, index) =>
241
273
  environment.subscribe(snapshot, latestSnapshot => {
242
- setState(existing => {
243
- invariant(
244
- existing.kind === 'plural',
245
- 'Cannot go from singular to plural or from bailout to plural.',
246
- );
247
- const updated = [...existing.snapshots];
274
+ setState(prevState => {
275
+ // In theory a setState from a subscription could be batched together
276
+ // with a setState to change the fragment selector. Guard against this
277
+ // by bailing out of the state update if the selector has changed.
278
+ if (
279
+ prevState.kind !== 'plural' ||
280
+ prevState.snapshots[index]?.selector !== latestSnapshot.selector
281
+ ) {
282
+ return prevState;
283
+ }
284
+ const updated = [...prevState.snapshots];
248
285
  updated[index] = latestSnapshot;
249
286
  return {
250
287
  kind: 'plural',
@@ -265,11 +302,12 @@ function subscribeToSnapshot(
265
302
  function getFragmentState(
266
303
  environment: IEnvironment,
267
304
  fragmentSelector: ?ReaderSelector,
268
- isPlural: boolean,
269
305
  ): FragmentState {
270
306
  if (fragmentSelector == null) {
271
307
  return {kind: 'bailout'};
272
308
  } else if (fragmentSelector.kind === 'PluralReaderSelector') {
309
+ // Note that if fragmentRef is an empty array, fragmentSelector will be null so we'll hit the above case.
310
+ // Null is returned by getSelector if fragmentRef has no non-null items.
273
311
  return {
274
312
  kind: 'plural',
275
313
  snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
@@ -345,29 +383,17 @@ function useFragmentInternal_REACT_CACHE(
345
383
  );
346
384
 
347
385
  const environment = useRelayEnvironment();
348
- const [rawState, setState] = useState<FragmentState>(() =>
349
- getFragmentState(environment, fragmentSelector, isPlural),
386
+ const [_state, setState] = useState<FragmentState>(() =>
387
+ getFragmentState(environment, fragmentSelector),
350
388
  );
351
- // On second look this separate rawState may not be needed at all, it can just be
352
- // put into getFragmentState. Exception: can we properly handle the case where the
353
- // fragmentRef goes from non-null to null?
354
- const stateFromRawState = (state: FragmentState) => {
355
- if (fragmentRef == null) {
356
- return {kind: 'bailout'};
357
- } else if (state.kind === 'plural' && state.snapshots.length === 0) {
358
- return {kind: 'bailout'};
359
- } else {
360
- return state;
361
- }
362
- };
363
- let state = stateFromRawState(rawState);
389
+ let state = _state;
364
390
 
365
391
  // This copy of the state we only update when something requires us to
366
392
  // unsubscribe and re-subscribe, namely a changed environment or
367
393
  // fragment selector.
368
- const [rawSubscribedState, setSubscribedState] = useState(state);
394
+ const [_subscribedState, setSubscribedState] = useState(state);
369
395
  // FIXME since this is used as an effect dependency, it needs to be memoized.
370
- let subscribedState = stateFromRawState(rawSubscribedState);
396
+ let subscribedState = _subscribedState;
371
397
 
372
398
  const [previousFragmentSelector, setPreviousFragmentSelector] =
373
399
  useState(fragmentSelector);
@@ -379,9 +405,7 @@ function useFragmentInternal_REACT_CACHE(
379
405
  // Enqueue setState to record the new selector and state
380
406
  setPreviousFragmentSelector(fragmentSelector);
381
407
  setPreviousEnvironment(environment);
382
- const newState = stateFromRawState(
383
- getFragmentState(environment, fragmentSelector, isPlural),
384
- );
408
+ const newState = getFragmentState(environment, fragmentSelector);
385
409
  setState(newState);
386
410
  setSubscribedState(newState); // This causes us to form a new subscription
387
411
  // But render with the latest state w/o waiting for the setState. Otherwise
@@ -391,6 +415,17 @@ function useFragmentInternal_REACT_CACHE(
391
415
  subscribedState = newState;
392
416
  }
393
417
 
418
+ // The purpose of this is to detect whether we have ever committed, because we
419
+ // don't suspend on store updates, only when the component either is first trying
420
+ // to mount or when the our selector changes. The selector change in particular is
421
+ // how we suspend for pagination and refetech. Also, fragment selector can be null
422
+ // or undefined, so we use false as a special value to distinguish from all fragment
423
+ // selectors; false means that the component hasn't mounted yet.
424
+ const committedFragmentSelectorRef = useRef<false | ?ReaderSelector>(false);
425
+ useEffect(() => {
426
+ committedFragmentSelectorRef.current = fragmentSelector;
427
+ }, [fragmentSelector]);
428
+
394
429
  // Handle the queries for any missing client edges; this may suspend.
395
430
  // FIXME handle client edges in parallel.
396
431
  if (fragmentNode.metadata?.hasClientEdges === true) {
@@ -398,14 +433,14 @@ function useFragmentInternal_REACT_CACHE(
398
433
  // a static (constant) property of the fragment. In practice, this effect will
399
434
  // always or never run for a given invocation of this hook.
400
435
  // eslint-disable-next-line react-hooks/rules-of-hooks
401
- const effects = useMemo(() => {
436
+ const clientEdgeQueries = useMemo(() => {
402
437
  const missingClientEdges = getMissingClientEdges(state);
403
438
  // eslint-disable-next-line no-shadow
404
- let effects;
439
+ let clientEdgeQueries;
405
440
  if (missingClientEdges?.length) {
406
- effects = [];
441
+ clientEdgeQueries = ([]: Array<QueryResult>);
407
442
  for (const edge of missingClientEdges) {
408
- effects.push(
443
+ clientEdgeQueries.push(
409
444
  handleMissingClientEdge(
410
445
  environment,
411
446
  fragmentNode,
@@ -416,41 +451,59 @@ function useFragmentInternal_REACT_CACHE(
416
451
  );
417
452
  }
418
453
  }
419
- return effects;
454
+ return clientEdgeQueries;
420
455
  }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
421
456
 
422
457
  // See above note
423
458
  // eslint-disable-next-line react-hooks/rules-of-hooks
424
459
  useEffect(() => {
425
- if (effects?.length) {
426
- const cleanups = [];
427
- for (const effect of effects) {
428
- cleanups.push(effect());
460
+ const QueryResource = getQueryResourceForEnvironment(environment);
461
+ if (clientEdgeQueries?.length) {
462
+ const disposables = [];
463
+ for (const query of clientEdgeQueries) {
464
+ disposables.push(QueryResource.retain(query));
429
465
  }
430
466
  return () => {
431
- for (const cleanup of cleanups) {
432
- cleanup();
467
+ for (const disposable of disposables) {
468
+ disposable.dispose();
433
469
  }
434
470
  };
435
471
  }
436
- }, [effects]);
472
+ }, [environment, clientEdgeQueries]);
437
473
  }
438
474
 
439
475
  if (isMissingData(state)) {
476
+ // Suspend if a Live Resolver within this fragment is in a suspended state:
477
+ const suspendingLiveResolvers = getSuspendingLiveResolver(state);
478
+ if (suspendingLiveResolvers != null && suspendingLiveResolvers.length > 0) {
479
+ throw Promise.all(
480
+ suspendingLiveResolvers.map(({liveStateID}) => {
481
+ // $FlowFixMe[prop-missing] This is expected to be a LiveResolverStore
482
+ return environment.getStore().getLiveResolverPromise(liveStateID);
483
+ }),
484
+ );
485
+ }
440
486
  // Suspend if an active operation bears on this fragment, either the
441
- // fragment's owner or some other mutation etc. that could affect it:
442
- invariant(fragmentSelector != null, 'refinement, see invariants above');
443
- const fragmentOwner =
444
- fragmentSelector.kind === 'PluralReaderSelector'
445
- ? fragmentSelector.selectors[0].owner
446
- : fragmentSelector.owner;
447
- const pendingOperationsResult = getPendingOperationsForFragment(
448
- environment,
449
- fragmentNode,
450
- fragmentOwner,
451
- );
452
- if (pendingOperationsResult) {
453
- throw pendingOperationsResult.promise;
487
+ // fragment's owner or some other mutation etc. that could affect it.
488
+ // We only suspend when the component is first trying to mount or changing
489
+ // selectors, not if data becomes missing later:
490
+ if (
491
+ !committedFragmentSelectorRef.current ||
492
+ !areEqualSelectors(committedFragmentSelectorRef.current, fragmentSelector)
493
+ ) {
494
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
495
+ const fragmentOwner =
496
+ fragmentSelector.kind === 'PluralReaderSelector'
497
+ ? fragmentSelector.selectors[0].owner
498
+ : fragmentSelector.owner;
499
+ const pendingOperationsResult = getPendingOperationsForFragment(
500
+ environment,
501
+ fragmentNode,
502
+ fragmentOwner,
503
+ );
504
+ if (pendingOperationsResult) {
505
+ throw pendingOperationsResult.promise;
506
+ }
454
507
  }
455
508
  // Report required fields only if we're not suspending, since that means
456
509
  // they're missing even though we are out of options for possibly fetching them:
@@ -475,23 +528,7 @@ function useFragmentInternal_REACT_CACHE(
475
528
  }
476
529
  currentState = updatedState;
477
530
  }
478
- return subscribeToSnapshot(environment, currentState, updater => {
479
- setState(latestState => {
480
- if (
481
- latestState.snapshot?.selector !== currentState.snapshot?.selector
482
- ) {
483
- // Ignore updates to the subscription if it's for a previous fragment selector
484
- // than the latest one to be rendered. This can happen if the store is updated
485
- // after we re-render with a new fragmentRef prop but before the effect fires
486
- // in which we unsubscribe to the old one and subscribe to the new one.
487
- // (NB: it's safe to compare the selectors by reference because the selector
488
- // is recycled into new snapshots.)
489
- return latestState;
490
- } else {
491
- return updater(latestState);
492
- }
493
- });
494
- });
531
+ return subscribeToSnapshot(environment, currentState, setState);
495
532
  }, [environment, subscribedState]);
496
533
 
497
534
  let data: ?SelectorData | Array<?SelectorData>;
@@ -501,10 +538,13 @@ function useFragmentInternal_REACT_CACHE(
501
538
  //
502
539
  // Note that isPlural is a constant property of the fragment and does not change
503
540
  // for a particular useFragment invocation site
541
+ const fragmentRefIsNullish = fragmentRef == null; // for less sensitive memoization
504
542
  // eslint-disable-next-line react-hooks/rules-of-hooks
505
543
  data = useMemo(() => {
506
544
  if (state.kind === 'bailout') {
507
- return [];
545
+ // Bailout state can happen if the fragmentRef is a plural array that is empty or has no
546
+ // non-null entries. In that case, the compatible behavior is to return [] instead of null.
547
+ return fragmentRefIsNullish ? null : [];
508
548
  } else {
509
549
  invariant(
510
550
  state.kind === 'plural',
@@ -512,7 +552,7 @@ function useFragmentInternal_REACT_CACHE(
512
552
  );
513
553
  return state.snapshots.map(s => s.data);
514
554
  }
515
- }, [state]);
555
+ }, [state, fragmentRefIsNullish]);
516
556
  } else if (state.kind === 'bailout') {
517
557
  // This case doesn't allocate a new object so it doesn't have to be memoized
518
558
  data = null;
@@ -4,13 +4,11 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @emails oncall+relay
8
7
  * @flow strict-local
9
8
  * @format
9
+ * @oncall relay
10
10
  */
11
11
 
12
- // flowlint ambiguous-object-type:error
13
-
14
12
  'use strict';
15
13
 
16
14
  import type {Fragment, FragmentType, GraphQLTaggedNode} from 'relay-runtime';
@@ -5,12 +5,10 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  * @flow strict-local
8
- * @emails oncall+relay
9
8
  * @format
9
+ * @oncall relay
10
10
  */
11
11
 
12
- // flowlint ambiguous-object-type:error
13
-
14
12
  'use strict';
15
13
 
16
14
  import type {
@@ -31,12 +29,12 @@ const {useEffect} = require('react');
31
29
  function useLazyLoadQuery_REACT_CACHE<TVariables: Variables, TData>(
32
30
  gqlQuery: Query<TVariables, TData>,
33
31
  variables: TVariables,
34
- options?: {|
32
+ options?: {
35
33
  fetchKey?: string | number,
36
34
  fetchPolicy?: FetchPolicy,
37
35
  networkCacheConfig?: CacheConfig,
38
36
  UNSTABLE_renderPolicy?: RenderPolicy,
39
- |},
37
+ },
40
38
  ): TData {
41
39
  useTrackLoadQueryInRender();
42
40
  const environment = useRelayEnvironment();
@@ -0,0 +1,190 @@
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 {LoadMoreFn, UseLoadMoreFunctionArgs} from '../useLoadMoreFunction';
15
+ import type {Options} from './useRefetchableFragmentInternal_REACT_CACHE';
16
+ import type {RefetchFnDynamic} from './useRefetchableFragmentInternal_REACT_CACHE';
17
+ import type {
18
+ FragmentType,
19
+ GraphQLResponse,
20
+ GraphQLTaggedNode,
21
+ Observer,
22
+ OperationType,
23
+ Variables,
24
+ } from 'relay-runtime';
25
+ import type {VariablesOf} from 'relay-runtime/util/RelayRuntimeTypes';
26
+
27
+ const useLoadMoreFunction = require('../useLoadMoreFunction');
28
+ const useRelayEnvironment = require('../useRelayEnvironment');
29
+ const useStaticFragmentNodeWarning = require('../useStaticFragmentNodeWarning');
30
+ const useRefetchableFragmentInternal = require('./useRefetchableFragmentInternal_REACT_CACHE');
31
+ const {useCallback, useDebugValue, useState} = require('react');
32
+ const {
33
+ getFragment,
34
+ getFragmentIdentifier,
35
+ getPaginationMetadata,
36
+ } = require('relay-runtime');
37
+
38
+ export type ReturnType<TQuery: OperationType, TKey, TFragmentData> = {
39
+ data: TFragmentData,
40
+ loadNext: LoadMoreFn<TQuery['variables']>,
41
+ loadPrevious: LoadMoreFn<TQuery['variables']>,
42
+ hasNext: boolean,
43
+ hasPrevious: boolean,
44
+ isLoadingNext: boolean,
45
+ isLoadingPrevious: boolean,
46
+ refetch: RefetchFnDynamic<TQuery, TKey>,
47
+ };
48
+
49
+ function usePaginationFragment<
50
+ TQuery: OperationType,
51
+ TKey: ?{+$data?: mixed, +$fragmentSpreads: FragmentType, ...},
52
+ >(
53
+ fragmentInput: GraphQLTaggedNode,
54
+ parentFragmentRef: TKey,
55
+ ): ReturnType<
56
+ TQuery,
57
+ TKey,
58
+ // NOTE: This $Call ensures that the type of the returned data is either:
59
+ // - nullable if the provided ref type is nullable
60
+ // - non-nullable if the provided ref type is non-nullable
61
+ // prettier-ignore
62
+ $Call<
63
+ & (<TFragmentData>( { +$data?: TFragmentData, ... }) => TFragmentData)
64
+ & (<TFragmentData>(?{ +$data?: TFragmentData, ... }) => ?TFragmentData),
65
+ TKey,
66
+ >,
67
+ > {
68
+ const fragmentNode = getFragment(fragmentInput);
69
+ useStaticFragmentNodeWarning(
70
+ fragmentNode,
71
+ 'first argument of usePaginationFragment()',
72
+ );
73
+ const componentDisplayName = 'usePaginationFragment()';
74
+
75
+ const {
76
+ connectionPathInFragmentData,
77
+ paginationRequest,
78
+ paginationMetadata,
79
+ identifierField,
80
+ } = getPaginationMetadata(fragmentNode, componentDisplayName);
81
+
82
+ const {fragmentData, fragmentRef, refetch} = useRefetchableFragmentInternal<
83
+ TQuery,
84
+ TKey,
85
+ >(fragmentNode, parentFragmentRef, componentDisplayName);
86
+ const fragmentIdentifier = getFragmentIdentifier(fragmentNode, fragmentRef);
87
+
88
+ // Backward pagination
89
+ const [loadPrevious, hasPrevious, isLoadingPrevious, disposeFetchPrevious] =
90
+ useLoadMore<TQuery['variables']>({
91
+ componentDisplayName,
92
+ connectionPathInFragmentData,
93
+ direction: 'backward',
94
+ fragmentData,
95
+ fragmentIdentifier,
96
+ fragmentNode,
97
+ fragmentRef,
98
+ identifierField,
99
+ paginationMetadata,
100
+ paginationRequest,
101
+ });
102
+
103
+ // Forward pagination
104
+ const [loadNext, hasNext, isLoadingNext, disposeFetchNext] = useLoadMore<
105
+ TQuery['variables'],
106
+ >({
107
+ componentDisplayName,
108
+ connectionPathInFragmentData,
109
+ direction: 'forward',
110
+ fragmentData,
111
+ fragmentIdentifier,
112
+ fragmentNode,
113
+ fragmentRef,
114
+ identifierField,
115
+ paginationMetadata,
116
+ paginationRequest,
117
+ });
118
+
119
+ const refetchPagination: RefetchFnDynamic<TQuery, TKey> = useCallback(
120
+ (variables: VariablesOf<TQuery>, options: void | Options) => {
121
+ disposeFetchNext();
122
+ disposeFetchPrevious();
123
+ return refetch(variables, {...options, __environment: undefined});
124
+ },
125
+ [disposeFetchNext, disposeFetchPrevious, refetch],
126
+ );
127
+
128
+ if (__DEV__) {
129
+ // eslint-disable-next-line react-hooks/rules-of-hooks
130
+ useDebugValue({
131
+ fragment: fragmentNode.name,
132
+ data: fragmentData,
133
+ hasNext,
134
+ isLoadingNext,
135
+ hasPrevious,
136
+ isLoadingPrevious,
137
+ });
138
+ }
139
+ return {
140
+ data: fragmentData,
141
+ loadNext,
142
+ loadPrevious,
143
+ hasNext,
144
+ hasPrevious,
145
+ isLoadingNext,
146
+ isLoadingPrevious,
147
+ refetch: refetchPagination,
148
+ };
149
+ }
150
+
151
+ function useLoadMore<TVariables: Variables>(
152
+ args: $Diff<
153
+ UseLoadMoreFunctionArgs,
154
+ {
155
+ observer: Observer<GraphQLResponse>,
156
+ onReset: () => void,
157
+ ...
158
+ },
159
+ >,
160
+ ): [LoadMoreFn<TVariables>, boolean, boolean, () => void] {
161
+ const environment = useRelayEnvironment();
162
+ const [isLoadingMore, reallySetIsLoadingMore] = useState(false);
163
+ // Schedule this update since it must be observed by components at the same
164
+ // batch as when hasNext changes. hasNext is read from the store and store
165
+ // updates are scheduled, so this must be scheduled too.
166
+ const setIsLoadingMore = (value: boolean) => {
167
+ const schedule = environment.getScheduler()?.schedule;
168
+ if (schedule) {
169
+ schedule(() => {
170
+ reallySetIsLoadingMore(value);
171
+ });
172
+ } else {
173
+ reallySetIsLoadingMore(value);
174
+ }
175
+ };
176
+ const observer = {
177
+ start: () => setIsLoadingMore(true),
178
+ complete: () => setIsLoadingMore(false),
179
+ error: () => setIsLoadingMore(false),
180
+ };
181
+ const handleReset = () => setIsLoadingMore(false);
182
+ const [loadMore, hasMore, disposeFetch] = useLoadMoreFunction<TVariables>({
183
+ ...args,
184
+ observer,
185
+ onReset: handleReset,
186
+ });
187
+ return [loadMore, hasMore, isLoadingMore, disposeFetch];
188
+ }
189
+
190
+ module.exports = usePaginationFragment;
@@ -5,12 +5,10 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
7
  * @flow strict-local
8
- * @emails oncall+relay
9
8
  * @format
9
+ * @oncall relay
10
10
  */
11
11
 
12
- // flowlint ambiguous-object-type:error
13
-
14
12
  'use strict';
15
13
 
16
14
  import type {PreloadedQuery} from '../EntryPointTypes.flow';
@@ -35,9 +33,9 @@ const warning = require('warning');
35
33
  function usePreloadedQuery_REACT_CACHE<TQuery: OperationType>(
36
34
  gqlQuery: GraphQLTaggedNode,
37
35
  preloadedQuery: PreloadedQuery<TQuery>,
38
- options?: {|
36
+ options?: {
39
37
  UNSTABLE_renderPolicy?: RenderPolicy,
40
- |},
38
+ },
41
39
  ): TQuery['response'] {
42
40
  const environment = useRelayEnvironment();
43
41
 
@@ -124,7 +122,6 @@ function usePreloadedQuery_REACT_CACHE<TQuery: OperationType>(
124
122
 
125
123
  // Read the query's root fragment -- this may suspend.
126
124
  const {fragmentNode, fragmentRef} = queryResult;
127
- // $FlowExpectedError[incompatible-return] Is this a fixable incompatible-return?
128
125
  const data = useFragmentInternal(
129
126
  fragmentNode,
130
127
  fragmentRef,