react-relay 14.0.0 → 14.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. package/ReactRelayContainerUtils.js.flow +0 -2
  2. package/ReactRelayContext.js +1 -1
  3. package/ReactRelayContext.js.flow +0 -2
  4. package/ReactRelayFragmentContainer.js.flow +0 -2
  5. package/ReactRelayFragmentMockRenderer.js.flow +0 -2
  6. package/ReactRelayLocalQueryRenderer.js.flow +0 -2
  7. package/ReactRelayPaginationContainer.js.flow +0 -2
  8. package/ReactRelayQueryFetcher.js.flow +9 -11
  9. package/ReactRelayQueryRenderer.js.flow +10 -12
  10. package/ReactRelayQueryRendererContext.js.flow +1 -3
  11. package/ReactRelayRefetchContainer.js.flow +0 -4
  12. package/ReactRelayTestMocker.js.flow +0 -2
  13. package/ReactRelayTypes.js.flow +6 -8
  14. package/RelayContext.js.flow +0 -2
  15. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +2 -4
  16. package/__flowtests__/ReactRelayPaginationContainer-flowtest.js.flow +3 -5
  17. package/__flowtests__/ReactRelayRefetchContainer-flowtest.js.flow +3 -5
  18. package/__flowtests__/RelayModern-flowtest.js.flow +2 -4
  19. package/__flowtests__/RelayModernFlowtest_badref.graphql.js.flow +2 -4
  20. package/__flowtests__/RelayModernFlowtest_notref.graphql.js.flow +2 -4
  21. package/__flowtests__/RelayModernFlowtest_user.graphql.js.flow +2 -4
  22. package/__flowtests__/RelayModernFlowtest_users.graphql.js.flow +2 -4
  23. package/assertFragmentMap.js.flow +0 -2
  24. package/buildReactRelayContainer.js.flow +0 -2
  25. package/getRootVariablesForFragments.js.flow +0 -2
  26. package/hooks.js +1 -1
  27. package/hooks.js.flow +0 -2
  28. package/index.js +1 -1
  29. package/index.js.flow +2 -2
  30. package/isRelayEnvironment.js.flow +0 -2
  31. package/legacy.js +1 -1
  32. package/legacy.js.flow +0 -2
  33. package/lib/ReactRelayContainerUtils.js +0 -1
  34. package/lib/ReactRelayContext.js +0 -1
  35. package/lib/ReactRelayFragmentContainer.js +10 -9
  36. package/lib/ReactRelayFragmentMockRenderer.js +0 -1
  37. package/lib/ReactRelayLocalQueryRenderer.js +0 -1
  38. package/lib/ReactRelayPaginationContainer.js +14 -11
  39. package/lib/ReactRelayQueryFetcher.js +1 -2
  40. package/lib/ReactRelayQueryRenderer.js +1 -2
  41. package/lib/ReactRelayQueryRendererContext.js +0 -1
  42. package/lib/ReactRelayRefetchContainer.js +11 -14
  43. package/lib/ReactRelayTestMocker.js +1 -2
  44. package/lib/ReactRelayTypes.js +0 -1
  45. package/lib/RelayContext.js +0 -1
  46. package/lib/assertFragmentMap.js +0 -1
  47. package/lib/buildReactRelayContainer.js +1 -2
  48. package/lib/getRootVariablesForFragments.js +1 -2
  49. package/lib/hooks.js +0 -1
  50. package/lib/index.js +3 -1
  51. package/lib/isRelayEnvironment.js +0 -1
  52. package/lib/legacy.js +0 -1
  53. package/lib/multi-actor/useRelayActorEnvironment.js +0 -1
  54. package/lib/readContext.js +0 -1
  55. package/lib/relay-hooks/EntryPointContainer.react.js +0 -1
  56. package/lib/relay-hooks/EntryPointTypes.flow.js +0 -1
  57. package/lib/relay-hooks/FragmentResource.js +7 -7
  58. package/lib/relay-hooks/HooksImplementation.js +1 -1
  59. package/lib/relay-hooks/InternalLogger.js +0 -1
  60. package/lib/relay-hooks/LRUCache.js +0 -1
  61. package/lib/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js +0 -1
  62. package/lib/relay-hooks/MatchContainer.js +1 -2
  63. package/lib/relay-hooks/ProfilerContext.js +0 -1
  64. package/lib/relay-hooks/QueryResource.js +1 -2
  65. package/lib/relay-hooks/RelayEnvironmentProvider.js +0 -1
  66. package/lib/relay-hooks/SuspenseResource.js +1 -2
  67. package/lib/relay-hooks/loadQuery.js +1 -1
  68. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +1 -2
  69. package/lib/relay-hooks/prepareEntryPoint_DEPRECATED.js +0 -1
  70. package/lib/relay-hooks/react-cache/RelayReactCache.js +0 -1
  71. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +3 -3
  72. package/lib/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js +239 -0
  73. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +183 -125
  74. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +0 -1
  75. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +0 -1
  76. package/lib/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js +150 -0
  77. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +0 -1
  78. package/lib/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js +367 -0
  79. package/lib/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js +45 -0
  80. package/lib/relay-hooks/useBlockingPaginationFragment.js +4 -3
  81. package/lib/relay-hooks/useClientQuery.js +33 -0
  82. package/lib/relay-hooks/useEntryPointLoader.js +1 -2
  83. package/lib/relay-hooks/useFetchTrackingRef.js +0 -1
  84. package/lib/relay-hooks/useFragment.js +0 -1
  85. package/lib/relay-hooks/useFragmentNode.js +0 -1
  86. package/lib/relay-hooks/useIsMountedRef.js +0 -1
  87. package/lib/relay-hooks/useLazyLoadQuery.js +1 -15
  88. package/lib/relay-hooks/useLazyLoadQueryNode.js +0 -1
  89. package/lib/relay-hooks/useLoadMoreFunction.js +1 -2
  90. package/lib/relay-hooks/useMemoOperationDescriptor.js +0 -1
  91. package/lib/relay-hooks/useMemoVariables.js +0 -1
  92. package/lib/relay-hooks/useMutation.js +1 -2
  93. package/lib/relay-hooks/usePaginationFragment.js +15 -3
  94. package/lib/relay-hooks/usePreloadedQuery.js +1 -15
  95. package/lib/relay-hooks/useQueryLoader.js +1 -2
  96. package/lib/relay-hooks/useRefetchableFragment.js +14 -2
  97. package/lib/relay-hooks/useRefetchableFragmentNode.js +1 -2
  98. package/lib/relay-hooks/useRelayEnvironment.js +0 -1
  99. package/lib/relay-hooks/useStaticFragmentNodeWarning.js +0 -1
  100. package/lib/relay-hooks/useSubscribeToInvalidationState.js +0 -1
  101. package/lib/relay-hooks/useSubscription.js +0 -1
  102. package/multi-actor/useRelayActorEnvironment.js.flow +0 -2
  103. package/package.json +2 -2
  104. package/react-relay-hooks.js +2 -2
  105. package/react-relay-hooks.min.js +2 -2
  106. package/react-relay-legacy.js +2 -2
  107. package/react-relay-legacy.min.js +2 -2
  108. package/react-relay.js +2 -2
  109. package/react-relay.min.js +2 -2
  110. package/readContext.js.flow +0 -2
  111. package/relay-hooks/EntryPointContainer.react.js.flow +2 -4
  112. package/relay-hooks/EntryPointTypes.flow.js.flow +30 -32
  113. package/relay-hooks/FragmentResource.js.flow +9 -11
  114. package/relay-hooks/HooksImplementation.js.flow +6 -8
  115. package/relay-hooks/InternalLogger.js.flow +0 -2
  116. package/relay-hooks/LRUCache.js.flow +0 -2
  117. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +4 -6
  118. package/relay-hooks/MatchContainer.js.flow +5 -7
  119. package/relay-hooks/ProfilerContext.js.flow +0 -2
  120. package/relay-hooks/QueryResource.js.flow +4 -6
  121. package/relay-hooks/RelayEnvironmentProvider.js.flow +2 -4
  122. package/relay-hooks/SuspenseResource.js.flow +0 -2
  123. package/relay-hooks/__flowtests__/EntryPointTypes/EntryPointElementConfig-flowtest.js.flow +3 -3
  124. package/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js.flow +2 -2
  125. package/relay-hooks/__flowtests__/useBlockingPaginationFragment-flowtest.js.flow +4 -6
  126. package/relay-hooks/__flowtests__/useFragment-flowtest.js.flow +0 -2
  127. package/relay-hooks/__flowtests__/usePaginationFragment-flowtest.js.flow +4 -6
  128. package/relay-hooks/__flowtests__/useRefetchableFragment-flowtest.js.flow +0 -2
  129. package/relay-hooks/__flowtests__/utils.js.flow +8 -10
  130. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +4 -6
  131. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +0 -2
  132. package/relay-hooks/react-cache/RelayReactCache.js.flow +0 -2
  133. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +22 -16
  134. package/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js.flow +297 -0
  135. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +134 -94
  136. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +0 -2
  137. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +2 -4
  138. package/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js.flow +171 -0
  139. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +2 -4
  140. package/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js.flow +595 -0
  141. package/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js.flow +65 -0
  142. package/relay-hooks/useBlockingPaginationFragment.js.flow +4 -6
  143. package/relay-hooks/useClientQuery.js.flow +39 -0
  144. package/relay-hooks/useEntryPointLoader.js.flow +6 -8
  145. package/relay-hooks/useFetchTrackingRef.js.flow +2 -4
  146. package/relay-hooks/useFragment.js.flow +0 -2
  147. package/relay-hooks/useFragmentNode.js.flow +2 -4
  148. package/relay-hooks/useIsMountedRef.js.flow +1 -3
  149. package/relay-hooks/useLazyLoadQuery.js.flow +7 -30
  150. package/relay-hooks/useLazyLoadQueryNode.js.flow +2 -4
  151. package/relay-hooks/useLoadMoreFunction.js.flow +6 -8
  152. package/relay-hooks/useMemoOperationDescriptor.js.flow +0 -2
  153. package/relay-hooks/useMemoVariables.js.flow +0 -2
  154. package/relay-hooks/useMutation.js.flow +2 -4
  155. package/relay-hooks/usePaginationFragment.js.flow +44 -19
  156. package/relay-hooks/usePreloadedQuery.js.flow +5 -24
  157. package/relay-hooks/useQueryLoader.js.flow +4 -6
  158. package/relay-hooks/useRefetchableFragment.js.flow +32 -3
  159. package/relay-hooks/useRefetchableFragmentNode.js.flow +16 -18
  160. package/relay-hooks/useRelayEnvironment.js.flow +0 -2
  161. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +0 -2
  162. package/relay-hooks/useSubscribeToInvalidationState.js.flow +0 -2
  163. package/relay-hooks/useSubscription.js.flow +0 -2
@@ -0,0 +1,297 @@
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
+ * @emails oncall+relay
9
+ * @format
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {QueryResult} from '../QueryResource';
15
+ import type {
16
+ CacheConfig,
17
+ FetchPolicy,
18
+ IEnvironment,
19
+ ReaderFragment,
20
+ ReaderSelector,
21
+ SelectorData,
22
+ Snapshot,
23
+ } from 'relay-runtime';
24
+ import type {MissingClientEdgeRequestInfo} from 'relay-runtime/store/RelayStoreTypes';
25
+
26
+ const {getQueryResourceForEnvironment} = require('../QueryResource');
27
+ const invariant = require('invariant');
28
+ const {
29
+ __internal: {fetchQuery: fetchQueryInternal},
30
+ createOperationDescriptor,
31
+ getPendingOperationsForFragment,
32
+ getSelector,
33
+ getVariablesFromFragment,
34
+ handlePotentialSnapshotErrors,
35
+ } = require('relay-runtime');
36
+ const warning = require('warning');
37
+
38
+ type FragmentQueryOptions = {
39
+ fetchPolicy?: FetchPolicy,
40
+ networkCacheConfig?: ?CacheConfig,
41
+ };
42
+
43
+ type FragmentState = $ReadOnly<
44
+ | {kind: 'bailout'}
45
+ | {kind: 'singular', snapshot: Snapshot, epoch: number}
46
+ | {kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number},
47
+ >;
48
+
49
+ function isMissingData(state: FragmentState): boolean {
50
+ if (state.kind === 'bailout') {
51
+ return false;
52
+ } else if (state.kind === 'singular') {
53
+ return state.snapshot.isMissingData;
54
+ } else {
55
+ return state.snapshots.some(s => s.isMissingData);
56
+ }
57
+ }
58
+
59
+ function getMissingClientEdges(
60
+ state: FragmentState,
61
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
62
+ if (state.kind === 'bailout') {
63
+ return null;
64
+ } else if (state.kind === 'singular') {
65
+ return state.snapshot.missingClientEdges ?? null;
66
+ } else {
67
+ let edges = null;
68
+ for (const snapshot of state.snapshots) {
69
+ if (snapshot.missingClientEdges) {
70
+ edges = edges ?? [];
71
+ for (const edge of snapshot.missingClientEdges) {
72
+ edges.push(edge);
73
+ }
74
+ }
75
+ }
76
+ return edges;
77
+ }
78
+ }
79
+
80
+ function handlePotentialSnapshotErrorsForState(
81
+ environment: IEnvironment,
82
+ state: FragmentState,
83
+ ): void {
84
+ if (state.kind === 'singular') {
85
+ handlePotentialSnapshotErrors(
86
+ environment,
87
+ state.snapshot.missingRequiredFields,
88
+ state.snapshot.relayResolverErrors,
89
+ );
90
+ } else if (state.kind === 'plural') {
91
+ for (const snapshot of state.snapshots) {
92
+ handlePotentialSnapshotErrors(
93
+ environment,
94
+ snapshot.missingRequiredFields,
95
+ snapshot.relayResolverErrors,
96
+ );
97
+ }
98
+ }
99
+ }
100
+
101
+ function handleMissingClientEdge(
102
+ environment: IEnvironment,
103
+ parentFragmentNode: ReaderFragment,
104
+ parentFragmentRef: mixed,
105
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
106
+ queryOptions?: FragmentQueryOptions,
107
+ ): QueryResult {
108
+ const originalVariables = getVariablesFromFragment(
109
+ parentFragmentNode,
110
+ parentFragmentRef,
111
+ );
112
+ const variables = {
113
+ ...originalVariables,
114
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
115
+ };
116
+ const queryOperationDescriptor = createOperationDescriptor(
117
+ missingClientEdgeRequestInfo.request,
118
+ variables,
119
+ queryOptions?.networkCacheConfig,
120
+ );
121
+ // This may suspend. We don't need to do anything with the results; all we're
122
+ // doing here is started the query if needed and retaining and releasing it
123
+ // according to the component mount/suspense cycle; QueryResource
124
+ // already handles this by itself.
125
+ const QueryResource = getQueryResourceForEnvironment(environment);
126
+ return QueryResource.prepare(
127
+ queryOperationDescriptor,
128
+ fetchQueryInternal(environment, queryOperationDescriptor),
129
+ queryOptions?.fetchPolicy,
130
+ );
131
+ }
132
+
133
+ function getFragmentState(
134
+ environment: IEnvironment,
135
+ fragmentSelector: ?ReaderSelector,
136
+ ): FragmentState {
137
+ if (fragmentSelector == null) {
138
+ return {kind: 'bailout'};
139
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
140
+ if (fragmentSelector.selectors.length === 0) {
141
+ return {kind: 'bailout'};
142
+ } else {
143
+ return {
144
+ kind: 'plural',
145
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
146
+ epoch: environment.getStore().getEpoch(),
147
+ };
148
+ }
149
+ } else {
150
+ return {
151
+ kind: 'singular',
152
+ snapshot: environment.lookup(fragmentSelector),
153
+ epoch: environment.getStore().getEpoch(),
154
+ };
155
+ }
156
+ }
157
+
158
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
159
+ function readFragmentInternal_REACT_CACHE(
160
+ environment: IEnvironment,
161
+ fragmentNode: ReaderFragment,
162
+ fragmentRef: mixed,
163
+ hookDisplayName: string,
164
+ queryOptions?: FragmentQueryOptions,
165
+ fragmentKey?: string,
166
+ ): {
167
+ +data: ?SelectorData | Array<?SelectorData>,
168
+ +clientEdgeQueries: ?Array<QueryResult>,
169
+ } {
170
+ const fragmentSelector = getSelector(fragmentNode, fragmentRef);
171
+ const isPlural = fragmentNode?.metadata?.plural === true;
172
+
173
+ if (isPlural) {
174
+ invariant(
175
+ fragmentRef == null || Array.isArray(fragmentRef),
176
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
177
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
178
+ 'from fragment `%s` to allow the prop to be an object.',
179
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
180
+ fragmentNode.name,
181
+ typeof fragmentRef,
182
+ fragmentNode.name,
183
+ );
184
+ } else {
185
+ invariant(
186
+ !Array.isArray(fragmentRef),
187
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
188
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
189
+ 'to fragment `%s` to allow the prop to be an array.',
190
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
191
+ fragmentNode.name,
192
+ typeof fragmentRef,
193
+ fragmentNode.name,
194
+ );
195
+ }
196
+ invariant(
197
+ fragmentRef == null ||
198
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
199
+ fragmentSelector != null,
200
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
201
+ 'but the fragment reference was not found`. This is most ' +
202
+ 'likely the result of:\n' +
203
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
204
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
205
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
206
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
207
+ 'spread - then the fragment reference will not exist. ' +
208
+ 'In this case, pass `null` if the conditions for evaluating the ' +
209
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
210
+ fragmentNode.name,
211
+ fragmentNode.name,
212
+ hookDisplayName,
213
+ fragmentNode.name,
214
+ fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
215
+ hookDisplayName,
216
+ );
217
+
218
+ const state = getFragmentState(environment, fragmentSelector);
219
+
220
+ // Handle the queries for any missing client edges; this may suspend.
221
+ // FIXME handle client edges in parallel.
222
+ let clientEdgeQueries = null;
223
+ if (fragmentNode.metadata?.hasClientEdges === true) {
224
+ const missingClientEdges = getMissingClientEdges(state);
225
+ if (missingClientEdges?.length) {
226
+ clientEdgeQueries = [];
227
+ for (const edge of missingClientEdges) {
228
+ clientEdgeQueries.push(
229
+ handleMissingClientEdge(
230
+ environment,
231
+ fragmentNode,
232
+ fragmentRef,
233
+ edge,
234
+ queryOptions,
235
+ ),
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ if (isMissingData(state)) {
242
+ // Suspend if an active operation bears on this fragment, either the
243
+ // fragment's owner or some other mutation etc. that could affect it:
244
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
245
+ const fragmentOwner =
246
+ fragmentSelector.kind === 'PluralReaderSelector'
247
+ ? fragmentSelector.selectors[0].owner
248
+ : fragmentSelector.owner;
249
+ const pendingOperationsResult = getPendingOperationsForFragment(
250
+ environment,
251
+ fragmentNode,
252
+ fragmentOwner,
253
+ );
254
+ if (pendingOperationsResult) {
255
+ throw pendingOperationsResult.promise;
256
+ }
257
+ // Report required fields only if we're not suspending, since that means
258
+ // they're missing even though we are out of options for possibly fetching them:
259
+ handlePotentialSnapshotErrorsForState(environment, state);
260
+ }
261
+
262
+ let data: ?SelectorData | Array<?SelectorData>;
263
+ if (state.kind === 'bailout') {
264
+ data = isPlural ? [] : null;
265
+ } else if (state.kind === 'singular') {
266
+ data = state.snapshot.data;
267
+ } else {
268
+ data = state.snapshots.map(s => s.data);
269
+ }
270
+
271
+ if (__DEV__) {
272
+ if (
273
+ fragmentRef != null &&
274
+ (data === undefined ||
275
+ (Array.isArray(data) &&
276
+ data.length > 0 &&
277
+ data.every(d => d === undefined)))
278
+ ) {
279
+ warning(
280
+ false,
281
+ 'Relay: Expected to have been able to read non-null data for ' +
282
+ 'fragment `%s` declared in ' +
283
+ '`%s`, since fragment reference was non-null. ' +
284
+ "Make sure that that `%s`'s parent isn't " +
285
+ 'holding on to and/or passing a fragment reference for data that ' +
286
+ 'has been deleted.',
287
+ fragmentNode.name,
288
+ hookDisplayName,
289
+ hookDisplayName,
290
+ );
291
+ }
292
+ }
293
+
294
+ return {data, clientEdgeQueries};
295
+ }
296
+
297
+ module.exports = readFragmentInternal_REACT_CACHE;
@@ -9,10 +9,9 @@
9
9
  * @format
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 {
@@ -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;
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 = [];
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;
@@ -9,8 +9,6 @@
9
9
  * @format
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';