react-relay 14.0.0 → 15.0.0

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