react-relay 13.1.1 → 14.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) 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 +7 -6
  5. package/ReactRelayFragmentMockRenderer.js.flow +0 -2
  6. package/ReactRelayLocalQueryRenderer.js.flow +1 -3
  7. package/ReactRelayPaginationContainer.js.flow +13 -10
  8. package/ReactRelayQueryFetcher.js.flow +10 -11
  9. package/ReactRelayQueryRenderer.js.flow +15 -16
  10. package/ReactRelayQueryRendererContext.js.flow +1 -3
  11. package/ReactRelayRefetchContainer.js.flow +10 -7
  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/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +2 -2
  24. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +2 -2
  25. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +3 -3
  26. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -3
  27. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +3 -3
  28. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -3
  29. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +2 -2
  30. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +2 -2
  31. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +2 -2
  32. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +2 -2
  33. package/assertFragmentMap.js.flow +0 -2
  34. package/buildReactRelayContainer.js.flow +2 -4
  35. package/getRootVariablesForFragments.js.flow +0 -2
  36. package/hooks.js +1 -1
  37. package/hooks.js.flow +0 -2
  38. package/index.js +1 -1
  39. package/index.js.flow +2 -2
  40. package/isRelayEnvironment.js.flow +0 -2
  41. package/jest-react/internalAct.js.flow +25 -9
  42. package/legacy.js +1 -1
  43. package/legacy.js.flow +0 -2
  44. package/lib/ReactRelayContainerUtils.js +0 -1
  45. package/lib/ReactRelayContext.js +0 -1
  46. package/lib/ReactRelayFragmentContainer.js +10 -9
  47. package/lib/ReactRelayFragmentMockRenderer.js +0 -1
  48. package/lib/ReactRelayLocalQueryRenderer.js +0 -1
  49. package/lib/ReactRelayPaginationContainer.js +14 -11
  50. package/lib/ReactRelayQueryFetcher.js +2 -2
  51. package/lib/ReactRelayQueryRenderer.js +2 -4
  52. package/lib/ReactRelayQueryRendererContext.js +0 -1
  53. package/lib/ReactRelayRefetchContainer.js +11 -14
  54. package/lib/ReactRelayTestMocker.js +1 -2
  55. package/lib/ReactRelayTypes.js +0 -1
  56. package/lib/RelayContext.js +0 -1
  57. package/lib/assertFragmentMap.js +0 -1
  58. package/lib/buildReactRelayContainer.js +1 -2
  59. package/lib/getRootVariablesForFragments.js +1 -2
  60. package/lib/hooks.js +0 -1
  61. package/lib/index.js +3 -1
  62. package/lib/isRelayEnvironment.js +0 -1
  63. package/lib/jest-react/internalAct.js +24 -4
  64. package/lib/legacy.js +0 -1
  65. package/lib/multi-actor/useRelayActorEnvironment.js +0 -1
  66. package/lib/readContext.js +2 -2
  67. package/lib/relay-hooks/EntryPointContainer.react.js +0 -1
  68. package/lib/relay-hooks/EntryPointTypes.flow.js +0 -1
  69. package/lib/relay-hooks/FragmentResource.js +68 -29
  70. package/lib/relay-hooks/HooksImplementation.js +29 -0
  71. package/lib/relay-hooks/InternalLogger.js +0 -1
  72. package/lib/relay-hooks/LRUCache.js +0 -1
  73. package/lib/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js +0 -1
  74. package/lib/relay-hooks/MatchContainer.js +2 -2
  75. package/lib/relay-hooks/ProfilerContext.js +0 -1
  76. package/lib/relay-hooks/QueryResource.js +5 -168
  77. package/lib/relay-hooks/RelayEnvironmentProvider.js +0 -1
  78. package/lib/relay-hooks/SuspenseResource.js +1 -2
  79. package/lib/relay-hooks/loadQuery.js +1 -1
  80. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +8 -13
  81. package/lib/relay-hooks/prepareEntryPoint_DEPRECATED.js +0 -1
  82. package/lib/relay-hooks/react-cache/RelayReactCache.js +36 -0
  83. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +344 -0
  84. package/lib/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js +239 -0
  85. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +598 -0
  86. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +50 -0
  87. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +55 -0
  88. package/lib/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js +150 -0
  89. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +124 -0
  90. package/lib/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js +367 -0
  91. package/lib/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js +45 -0
  92. package/lib/relay-hooks/useBlockingPaginationFragment.js +4 -3
  93. package/lib/relay-hooks/useClientQuery.js +33 -0
  94. package/lib/relay-hooks/useEntryPointLoader.js +1 -2
  95. package/lib/relay-hooks/useFetchTrackingRef.js +0 -1
  96. package/lib/relay-hooks/useFragment.js +15 -2
  97. package/lib/relay-hooks/useFragmentNode.js +0 -1
  98. package/lib/relay-hooks/useIsMountedRef.js +0 -1
  99. package/lib/relay-hooks/useLazyLoadQuery.js +4 -2
  100. package/lib/relay-hooks/useLazyLoadQueryNode.js +0 -1
  101. package/lib/relay-hooks/useLoadMoreFunction.js +1 -2
  102. package/lib/relay-hooks/useMemoOperationDescriptor.js +0 -1
  103. package/lib/relay-hooks/useMemoVariables.js +0 -1
  104. package/lib/relay-hooks/useMutation.js +5 -7
  105. package/lib/relay-hooks/usePaginationFragment.js +15 -3
  106. package/lib/relay-hooks/usePreloadedQuery.js +4 -2
  107. package/lib/relay-hooks/useQueryLoader.js +1 -2
  108. package/lib/relay-hooks/useRefetchableFragment.js +14 -2
  109. package/lib/relay-hooks/useRefetchableFragmentNode.js +1 -2
  110. package/lib/relay-hooks/useRelayEnvironment.js +0 -1
  111. package/lib/relay-hooks/useStaticFragmentNodeWarning.js +0 -1
  112. package/lib/relay-hooks/useSubscribeToInvalidationState.js +0 -1
  113. package/lib/relay-hooks/useSubscription.js +0 -1
  114. package/multi-actor/useRelayActorEnvironment.js.flow +0 -2
  115. package/package.json +3 -3
  116. package/react-relay-hooks.js +2 -2
  117. package/react-relay-hooks.min.js +2 -2
  118. package/react-relay-legacy.js +2 -2
  119. package/react-relay-legacy.min.js +2 -2
  120. package/react-relay.js +2 -2
  121. package/react-relay.min.js +2 -2
  122. package/readContext.js.flow +1 -2
  123. package/relay-hooks/EntryPointContainer.react.js.flow +2 -4
  124. package/relay-hooks/EntryPointTypes.flow.js.flow +30 -32
  125. package/relay-hooks/FragmentResource.js.flow +80 -37
  126. package/relay-hooks/HooksImplementation.js.flow +43 -0
  127. package/relay-hooks/InternalLogger.js.flow +0 -2
  128. package/relay-hooks/LRUCache.js.flow +0 -2
  129. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +4 -6
  130. package/relay-hooks/MatchContainer.js.flow +11 -6
  131. package/relay-hooks/ProfilerContext.js.flow +0 -2
  132. package/relay-hooks/QueryResource.js.flow +12 -209
  133. package/relay-hooks/RelayEnvironmentProvider.js.flow +2 -4
  134. package/relay-hooks/SuspenseResource.js.flow +0 -2
  135. package/relay-hooks/__flowtests__/EntryPointTypes/EntryPointElementConfig-flowtest.js.flow +3 -3
  136. package/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js.flow +2 -2
  137. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +2 -2
  138. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +2 -2
  139. package/relay-hooks/__flowtests__/useBlockingPaginationFragment-flowtest.js.flow +4 -6
  140. package/relay-hooks/__flowtests__/useFragment-flowtest.js.flow +0 -2
  141. package/relay-hooks/__flowtests__/usePaginationFragment-flowtest.js.flow +4 -6
  142. package/relay-hooks/__flowtests__/useRefetchableFragment-flowtest.js.flow +0 -2
  143. package/relay-hooks/__flowtests__/utils.js.flow +8 -10
  144. package/relay-hooks/loadQuery.js.flow +2 -1
  145. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +11 -20
  146. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +0 -2
  147. package/relay-hooks/react-cache/RelayReactCache.js.flow +40 -0
  148. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +430 -0
  149. package/relay-hooks/react-cache/readFragmentInternal_REACT_CACHE.js.flow +297 -0
  150. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +599 -0
  151. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +72 -0
  152. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +70 -0
  153. package/relay-hooks/react-cache/usePaginationFragment_REACT_CACHE.js.flow +171 -0
  154. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +151 -0
  155. package/relay-hooks/react-cache/useRefetchableFragmentInternal_REACT_CACHE.js.flow +595 -0
  156. package/relay-hooks/react-cache/useRefetchableFragment_REACT_CACHE.js.flow +65 -0
  157. package/relay-hooks/useBlockingPaginationFragment.js.flow +4 -6
  158. package/relay-hooks/useClientQuery.js.flow +39 -0
  159. package/relay-hooks/useEntryPointLoader.js.flow +6 -8
  160. package/relay-hooks/useFetchTrackingRef.js.flow +2 -4
  161. package/relay-hooks/useFragment.js.flow +17 -12
  162. package/relay-hooks/useFragmentNode.js.flow +2 -4
  163. package/relay-hooks/useIsMountedRef.js.flow +1 -3
  164. package/relay-hooks/useLazyLoadQuery.js.flow +17 -5
  165. package/relay-hooks/useLazyLoadQueryNode.js.flow +2 -4
  166. package/relay-hooks/useLoadMoreFunction.js.flow +6 -8
  167. package/relay-hooks/useMemoOperationDescriptor.js.flow +0 -2
  168. package/relay-hooks/useMemoVariables.js.flow +0 -2
  169. package/relay-hooks/useMutation.js.flow +5 -7
  170. package/relay-hooks/usePaginationFragment.js.flow +44 -19
  171. package/relay-hooks/usePreloadedQuery.js.flow +14 -5
  172. package/relay-hooks/useQueryLoader.js.flow +4 -6
  173. package/relay-hooks/useRefetchableFragment.js.flow +32 -3
  174. package/relay-hooks/useRefetchableFragmentNode.js.flow +38 -25
  175. package/relay-hooks/useRelayEnvironment.js.flow +0 -2
  176. package/relay-hooks/useStaticFragmentNodeWarning.js.flow +0 -2
  177. package/relay-hooks/useSubscribeToInvalidationState.js.flow +0 -2
  178. package/relay-hooks/useSubscription.js.flow +14 -10
@@ -0,0 +1,599 @@
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 {
25
+ MissingClientEdgeRequestInfo,
26
+ MissingLiveResolverField,
27
+ } from 'relay-runtime/store/RelayStoreTypes';
28
+
29
+ const {getQueryResourceForEnvironment} = require('../QueryResource');
30
+ const useRelayEnvironment = require('../useRelayEnvironment');
31
+ const invariant = require('invariant');
32
+ const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
33
+ const {
34
+ __internal: {fetchQuery: fetchQueryInternal},
35
+ areEqualSelectors,
36
+ createOperationDescriptor,
37
+ getPendingOperationsForFragment,
38
+ getSelector,
39
+ getVariablesFromFragment,
40
+ handlePotentialSnapshotErrors,
41
+ recycleNodesInto,
42
+ } = require('relay-runtime');
43
+ const warning = require('warning');
44
+
45
+ type FragmentQueryOptions = {
46
+ fetchPolicy?: FetchPolicy,
47
+ networkCacheConfig?: ?CacheConfig,
48
+ };
49
+
50
+ type FragmentState = $ReadOnly<
51
+ | {kind: 'bailout'}
52
+ | {kind: 'singular', snapshot: Snapshot, epoch: number}
53
+ | {kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number},
54
+ >;
55
+
56
+ type StateUpdaterFunction<T> = ((T) => T) => void;
57
+
58
+ function isMissingData(state: FragmentState): boolean {
59
+ if (state.kind === 'bailout') {
60
+ return false;
61
+ } else if (state.kind === 'singular') {
62
+ return state.snapshot.isMissingData;
63
+ } else {
64
+ return state.snapshots.some(s => s.isMissingData);
65
+ }
66
+ }
67
+
68
+ function getMissingClientEdges(
69
+ state: FragmentState,
70
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
71
+ if (state.kind === 'bailout') {
72
+ return null;
73
+ } else if (state.kind === 'singular') {
74
+ return state.snapshot.missingClientEdges ?? null;
75
+ } else {
76
+ let edges = null;
77
+ for (const snapshot of state.snapshots) {
78
+ if (snapshot.missingClientEdges) {
79
+ edges = edges ?? [];
80
+ for (const edge of snapshot.missingClientEdges) {
81
+ edges.push(edge);
82
+ }
83
+ }
84
+ }
85
+ return edges;
86
+ }
87
+ }
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
+
110
+ function handlePotentialSnapshotErrorsForState(
111
+ environment: IEnvironment,
112
+ state: FragmentState,
113
+ ): void {
114
+ if (state.kind === 'singular') {
115
+ handlePotentialSnapshotErrors(
116
+ environment,
117
+ state.snapshot.missingRequiredFields,
118
+ state.snapshot.relayResolverErrors,
119
+ );
120
+ } else if (state.kind === 'plural') {
121
+ for (const snapshot of state.snapshots) {
122
+ handlePotentialSnapshotErrors(
123
+ environment,
124
+ snapshot.missingRequiredFields,
125
+ snapshot.relayResolverErrors,
126
+ );
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Check for updates to the store that occurred concurrently with rendering the given `state` value,
133
+ * returning a new (updated) state if there were updates or null if there were no changes.
134
+ */
135
+ function handleMissedUpdates(
136
+ environment: IEnvironment,
137
+ state: FragmentState,
138
+ ): null | [/* has data changed */ boolean, FragmentState] {
139
+ if (state.kind === 'bailout') {
140
+ return null;
141
+ }
142
+ // FIXME this is invalid if we've just switched environments.
143
+ const currentEpoch = environment.getStore().getEpoch();
144
+ if (currentEpoch === state.epoch) {
145
+ return null;
146
+ }
147
+ // The store has updated since we rendered (without us being subscribed yet),
148
+ // so check for any updates to the data we're rendering:
149
+ if (state.kind === 'singular') {
150
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
151
+ const updatedData = recycleNodesInto(
152
+ state.snapshot.data,
153
+ currentSnapshot.data,
154
+ );
155
+ const updatedCurrentSnapshot: Snapshot = {
156
+ data: updatedData,
157
+ isMissingData: currentSnapshot.isMissingData,
158
+ missingClientEdges: currentSnapshot.missingClientEdges,
159
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
160
+ seenRecords: currentSnapshot.seenRecords,
161
+ selector: currentSnapshot.selector,
162
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
163
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
164
+ };
165
+ return [
166
+ updatedData !== state.snapshot.data,
167
+ {
168
+ kind: 'singular',
169
+ snapshot: updatedCurrentSnapshot,
170
+ epoch: currentEpoch,
171
+ },
172
+ ];
173
+ } else {
174
+ let didMissUpdates = false;
175
+ const currentSnapshots = [];
176
+ for (let index = 0; index < state.snapshots.length; index++) {
177
+ const snapshot = state.snapshots[index];
178
+ const currentSnapshot = environment.lookup(snapshot.selector);
179
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
180
+ const updatedCurrentSnapshot: Snapshot = {
181
+ data: updatedData,
182
+ isMissingData: currentSnapshot.isMissingData,
183
+ missingClientEdges: currentSnapshot.missingClientEdges,
184
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
185
+ seenRecords: currentSnapshot.seenRecords,
186
+ selector: currentSnapshot.selector,
187
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
188
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
189
+ };
190
+ if (updatedData !== snapshot.data) {
191
+ didMissUpdates = true;
192
+ }
193
+ currentSnapshots.push(updatedCurrentSnapshot);
194
+ }
195
+ invariant(
196
+ currentSnapshots.length === state.snapshots.length,
197
+ 'Expected same number of snapshots',
198
+ );
199
+ return [
200
+ didMissUpdates,
201
+ {
202
+ kind: 'plural',
203
+ snapshots: currentSnapshots,
204
+ epoch: currentEpoch,
205
+ },
206
+ ];
207
+ }
208
+ }
209
+
210
+ function handleMissingClientEdge(
211
+ environment: IEnvironment,
212
+ parentFragmentNode: ReaderFragment,
213
+ parentFragmentRef: mixed,
214
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
215
+ queryOptions?: FragmentQueryOptions,
216
+ ): QueryResult {
217
+ const originalVariables = getVariablesFromFragment(
218
+ parentFragmentNode,
219
+ parentFragmentRef,
220
+ );
221
+ const variables = {
222
+ ...originalVariables,
223
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
224
+ };
225
+ const queryOperationDescriptor = createOperationDescriptor(
226
+ missingClientEdgeRequestInfo.request,
227
+ variables,
228
+ queryOptions?.networkCacheConfig,
229
+ );
230
+ // This may suspend. We don't need to do anything with the results; all we're
231
+ // doing here is started the query if needed and retaining and releasing it
232
+ // according to the component mount/suspense cycle; QueryResource
233
+ // already handles this by itself.
234
+ const QueryResource = getQueryResourceForEnvironment(environment);
235
+ return QueryResource.prepare(
236
+ queryOperationDescriptor,
237
+ fetchQueryInternal(environment, queryOperationDescriptor),
238
+ queryOptions?.fetchPolicy,
239
+ );
240
+ }
241
+
242
+ function subscribeToSnapshot(
243
+ environment: IEnvironment,
244
+ state: FragmentState,
245
+ setState: StateUpdaterFunction<FragmentState>,
246
+ ): () => void {
247
+ if (state.kind === 'bailout') {
248
+ return () => {};
249
+ } else if (state.kind === 'singular') {
250
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
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
+ });
267
+ });
268
+ return () => {
269
+ disposable.dispose();
270
+ };
271
+ } else {
272
+ const disposables = state.snapshots.map((snapshot, index) =>
273
+ environment.subscribe(snapshot, latestSnapshot => {
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];
285
+ updated[index] = latestSnapshot;
286
+ return {
287
+ kind: 'plural',
288
+ snapshots: updated,
289
+ epoch: environment.getStore().getEpoch(),
290
+ };
291
+ });
292
+ }),
293
+ );
294
+ return () => {
295
+ for (const d of disposables) {
296
+ d.dispose();
297
+ }
298
+ };
299
+ }
300
+ }
301
+
302
+ function getFragmentState(
303
+ environment: IEnvironment,
304
+ fragmentSelector: ?ReaderSelector,
305
+ ): FragmentState {
306
+ if (fragmentSelector == null) {
307
+ return {kind: 'bailout'};
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.
311
+ return {
312
+ kind: 'plural',
313
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
314
+ epoch: environment.getStore().getEpoch(),
315
+ };
316
+ } else {
317
+ return {
318
+ kind: 'singular',
319
+ snapshot: environment.lookup(fragmentSelector),
320
+ epoch: environment.getStore().getEpoch(),
321
+ };
322
+ }
323
+ }
324
+
325
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
326
+ function useFragmentInternal_REACT_CACHE(
327
+ fragmentNode: ReaderFragment,
328
+ fragmentRef: mixed,
329
+ hookDisplayName: string,
330
+ queryOptions?: FragmentQueryOptions,
331
+ fragmentKey?: string,
332
+ ): ?SelectorData | Array<?SelectorData> {
333
+ const fragmentSelector = useMemo(
334
+ () => getSelector(fragmentNode, fragmentRef),
335
+ [fragmentNode, fragmentRef],
336
+ );
337
+
338
+ const isPlural = fragmentNode?.metadata?.plural === true;
339
+
340
+ if (isPlural) {
341
+ invariant(
342
+ fragmentRef == null || Array.isArray(fragmentRef),
343
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
344
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
345
+ 'from fragment `%s` to allow the prop to be an object.',
346
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
347
+ fragmentNode.name,
348
+ typeof fragmentRef,
349
+ fragmentNode.name,
350
+ );
351
+ } else {
352
+ invariant(
353
+ !Array.isArray(fragmentRef),
354
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
355
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
356
+ 'to fragment `%s` to allow the prop to be an array.',
357
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
358
+ fragmentNode.name,
359
+ typeof fragmentRef,
360
+ fragmentNode.name,
361
+ );
362
+ }
363
+ invariant(
364
+ fragmentRef == null ||
365
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
366
+ fragmentSelector != null,
367
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
368
+ 'but the fragment reference was not found`. This is most ' +
369
+ 'likely the result of:\n' +
370
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
371
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
372
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
373
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
374
+ 'spread - then the fragment reference will not exist. ' +
375
+ 'In this case, pass `null` if the conditions for evaluating the ' +
376
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
377
+ fragmentNode.name,
378
+ fragmentNode.name,
379
+ hookDisplayName,
380
+ fragmentNode.name,
381
+ fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
382
+ hookDisplayName,
383
+ );
384
+
385
+ const environment = useRelayEnvironment();
386
+ const [_state, setState] = useState<FragmentState>(() =>
387
+ getFragmentState(environment, fragmentSelector),
388
+ );
389
+ let state = _state;
390
+
391
+ // This copy of the state we only update when something requires us to
392
+ // unsubscribe and re-subscribe, namely a changed environment or
393
+ // fragment selector.
394
+ const [_subscribedState, setSubscribedState] = useState(state);
395
+ // FIXME since this is used as an effect dependency, it needs to be memoized.
396
+ let subscribedState = _subscribedState;
397
+
398
+ const [previousFragmentSelector, setPreviousFragmentSelector] =
399
+ useState(fragmentSelector);
400
+ const [previousEnvironment, setPreviousEnvironment] = useState(environment);
401
+ if (
402
+ !areEqualSelectors(fragmentSelector, previousFragmentSelector) ||
403
+ environment !== previousEnvironment
404
+ ) {
405
+ // Enqueue setState to record the new selector and state
406
+ setPreviousFragmentSelector(fragmentSelector);
407
+ setPreviousEnvironment(environment);
408
+ const newState = getFragmentState(environment, fragmentSelector);
409
+ setState(newState);
410
+ setSubscribedState(newState); // This causes us to form a new subscription
411
+ // But render with the latest state w/o waiting for the setState. Otherwise
412
+ // the component would render the wrong information temporarily (including
413
+ // possibly incorrectly triggering some warnings below).
414
+ state = newState;
415
+ subscribedState = newState;
416
+ }
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
+
429
+ // Handle the queries for any missing client edges; this may suspend.
430
+ // FIXME handle client edges in parallel.
431
+ if (fragmentNode.metadata?.hasClientEdges === true) {
432
+ // The fragment is validated to be static (in useFragment) and hasClientEdges is
433
+ // a static (constant) property of the fragment. In practice, this effect will
434
+ // always or never run for a given invocation of this hook.
435
+ // eslint-disable-next-line react-hooks/rules-of-hooks
436
+ const clientEdgeQueries = useMemo(() => {
437
+ const missingClientEdges = getMissingClientEdges(state);
438
+ // eslint-disable-next-line no-shadow
439
+ let clientEdgeQueries;
440
+ if (missingClientEdges?.length) {
441
+ clientEdgeQueries = [];
442
+ for (const edge of missingClientEdges) {
443
+ clientEdgeQueries.push(
444
+ handleMissingClientEdge(
445
+ environment,
446
+ fragmentNode,
447
+ fragmentRef,
448
+ edge,
449
+ queryOptions,
450
+ ),
451
+ );
452
+ }
453
+ }
454
+ return clientEdgeQueries;
455
+ }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
456
+
457
+ // See above note
458
+ // eslint-disable-next-line react-hooks/rules-of-hooks
459
+ useEffect(() => {
460
+ const QueryResource = getQueryResourceForEnvironment(environment);
461
+ if (clientEdgeQueries?.length) {
462
+ const disposables = [];
463
+ for (const query of clientEdgeQueries) {
464
+ disposables.push(QueryResource.retain(query));
465
+ }
466
+ return () => {
467
+ for (const disposable of disposables) {
468
+ disposable.dispose();
469
+ }
470
+ };
471
+ }
472
+ }, [environment, clientEdgeQueries]);
473
+ }
474
+
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
+ }
486
+ // Suspend if an active operation bears on this fragment, either the
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
+ }
507
+ }
508
+ // Report required fields only if we're not suspending, since that means
509
+ // they're missing even though we are out of options for possibly fetching them:
510
+ handlePotentialSnapshotErrorsForState(environment, state);
511
+ }
512
+
513
+ useEffect(() => {
514
+ // Check for updates since the state was rendered
515
+ let currentState = subscribedState;
516
+ const updates = handleMissedUpdates(environment, subscribedState);
517
+ if (updates !== null) {
518
+ const [didMissUpdates, updatedState] = updates;
519
+ // TODO: didMissUpdates only checks for changes to snapshot data, but it's possible
520
+ // that other snapshot properties may have changed that should also trigger a re-render,
521
+ // such as changed missing resolver fields, missing client edges, etc.
522
+ // A potential alternative is for handleMissedUpdates() to recycle the entire state
523
+ // value, and return the new (recycled) state only if there was some change. In that
524
+ // case the code would always setState if something in the snapshot changed, in addition
525
+ // to using the latest snapshot to subscribe.
526
+ if (didMissUpdates) {
527
+ setState(updatedState);
528
+ }
529
+ currentState = updatedState;
530
+ }
531
+ return subscribeToSnapshot(environment, currentState, setState);
532
+ }, [environment, subscribedState]);
533
+
534
+ let data: ?SelectorData | Array<?SelectorData>;
535
+ if (isPlural) {
536
+ // Plural fragments require allocating an array of the snasphot data values,
537
+ // which has to be memoized to avoid triggering downstream re-renders.
538
+ //
539
+ // Note that isPlural is a constant property of the fragment and does not change
540
+ // for a particular useFragment invocation site
541
+ const fragmentRefIsNullish = fragmentRef == null; // for less sensitive memoization
542
+ // eslint-disable-next-line react-hooks/rules-of-hooks
543
+ data = useMemo(() => {
544
+ if (state.kind === 'bailout') {
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 : [];
548
+ } else {
549
+ invariant(
550
+ state.kind === 'plural',
551
+ 'Expected state to be plural because fragment is plural',
552
+ );
553
+ return state.snapshots.map(s => s.data);
554
+ }
555
+ }, [state, fragmentRefIsNullish]);
556
+ } else if (state.kind === 'bailout') {
557
+ // This case doesn't allocate a new object so it doesn't have to be memoized
558
+ data = null;
559
+ } else {
560
+ // This case doesn't allocate a new object so it doesn't have to be memoized
561
+ invariant(
562
+ state.kind === 'singular',
563
+ 'Expected state to be singular because fragment is singular',
564
+ );
565
+ data = state.snapshot.data;
566
+ }
567
+
568
+ if (__DEV__) {
569
+ if (
570
+ fragmentRef != null &&
571
+ (data === undefined ||
572
+ (Array.isArray(data) &&
573
+ data.length > 0 &&
574
+ data.every(d => d === undefined)))
575
+ ) {
576
+ warning(
577
+ false,
578
+ 'Relay: Expected to have been able to read non-null data for ' +
579
+ 'fragment `%s` declared in ' +
580
+ '`%s`, since fragment reference was non-null. ' +
581
+ "Make sure that that `%s`'s parent isn't " +
582
+ 'holding on to and/or passing a fragment reference for data that ' +
583
+ 'has been deleted.',
584
+ fragmentNode.name,
585
+ hookDisplayName,
586
+ hookDisplayName,
587
+ );
588
+ }
589
+ }
590
+
591
+ if (__DEV__) {
592
+ // eslint-disable-next-line react-hooks/rules-of-hooks
593
+ useDebugValue({fragment: fragmentNode.name, data});
594
+ }
595
+
596
+ return data;
597
+ }
598
+
599
+ module.exports = useFragmentInternal_REACT_CACHE;
@@ -0,0 +1,72 @@
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
+ * @emails oncall+relay
8
+ * @flow strict-local
9
+ * @format
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {Fragment, FragmentType, GraphQLTaggedNode} from 'relay-runtime';
15
+
16
+ const {useTrackLoadQueryInRender} = require('../loadQuery');
17
+ const useStaticFragmentNodeWarning = require('../useStaticFragmentNodeWarning');
18
+ const useFragmentInternal = require('./useFragmentInternal_REACT_CACHE');
19
+ const {useDebugValue} = require('react');
20
+ const {getFragment} = require('relay-runtime');
21
+
22
+ type HasSpread<TFragmentType> = {
23
+ +$fragmentSpreads: TFragmentType,
24
+ ...
25
+ };
26
+
27
+ // if the key is non-nullable, return non-nullable value
28
+ declare function useFragment<TFragmentType: FragmentType, TData>(
29
+ fragment: Fragment<TFragmentType, TData>,
30
+ key: HasSpread<TFragmentType>,
31
+ ): TData;
32
+
33
+ // if the key is nullable, return nullable value
34
+ declare function useFragment<TFragmentType: FragmentType, TData>(
35
+ fragment: Fragment<TFragmentType, TData>,
36
+ key: ?HasSpread<TFragmentType>,
37
+ ): ?TData;
38
+
39
+ // if the key is a non-nullable array of keys, return non-nullable array
40
+ declare function useFragment<TFragmentType: FragmentType, TData>(
41
+ fragment: Fragment<TFragmentType, TData>,
42
+ key: $ReadOnlyArray<HasSpread<TFragmentType>>,
43
+ ): TData;
44
+
45
+ // if the key is a nullable array of keys, return nullable array
46
+ declare function useFragment<TFragmentType: FragmentType, TData>(
47
+ fragment: Fragment<TFragmentType, TData>,
48
+ key: ?$ReadOnlyArray<HasSpread<TFragmentType>>,
49
+ ): ?TData;
50
+
51
+ function useFragment(fragment: GraphQLTaggedNode, key: mixed): mixed {
52
+ // We need to use this hook in order to be able to track if
53
+ // loadQuery was called during render
54
+ useTrackLoadQueryInRender();
55
+
56
+ const fragmentNode = getFragment(fragment);
57
+ if (__DEV__) {
58
+ // eslint-disable-next-line react-hooks/rules-of-hooks
59
+ useStaticFragmentNodeWarning(
60
+ fragmentNode,
61
+ 'first argument of useFragment()',
62
+ );
63
+ }
64
+ const data = useFragmentInternal(fragmentNode, key, 'useFragment()');
65
+ if (__DEV__) {
66
+ // eslint-disable-next-line react-hooks/rules-of-hooks
67
+ useDebugValue({fragment: fragmentNode.name, data});
68
+ }
69
+ return data;
70
+ }
71
+
72
+ module.exports = useFragment;