react-relay 13.1.1 → 13.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayLocalQueryRenderer.js.flow +1 -1
  3. package/ReactRelayQueryRenderer.js.flow +1 -4
  4. package/hooks.js +1 -1
  5. package/index.js +1 -1
  6. package/jest-react/internalAct.js.flow +25 -9
  7. package/legacy.js +1 -1
  8. package/lib/ReactRelayQueryRenderer.js +1 -1
  9. package/lib/jest-react/internalAct.js +24 -4
  10. package/lib/relay-hooks/FragmentResource.js +10 -13
  11. package/lib/relay-hooks/QueryResource.js +2 -165
  12. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +7 -11
  13. package/lib/relay-hooks/react-cache/RelayReactCache.js +37 -0
  14. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +197 -0
  15. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +395 -0
  16. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +45 -0
  17. package/package.json +3 -3
  18. package/react-relay-hooks.js +2 -2
  19. package/react-relay-hooks.min.js +2 -2
  20. package/react-relay-legacy.js +2 -2
  21. package/react-relay-legacy.min.js +2 -2
  22. package/react-relay.js +2 -2
  23. package/react-relay.min.js +2 -2
  24. package/relay-hooks/FragmentResource.js.flow +17 -18
  25. package/relay-hooks/QueryResource.js.flow +4 -201
  26. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +7 -14
  27. package/relay-hooks/react-cache/RelayReactCache.js.flow +42 -0
  28. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +243 -0
  29. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +416 -0
  30. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +66 -0
@@ -0,0 +1,416 @@
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
+ // flowlint ambiguous-object-type:error
13
+
14
+ 'use strict';
15
+
16
+ import type {
17
+ CacheConfig,
18
+ FetchPolicy,
19
+ IEnvironment,
20
+ ReaderFragment,
21
+ ReaderSelector,
22
+ SelectorData,
23
+ Snapshot,
24
+ } from 'relay-runtime';
25
+ import type {MissingClientEdgeRequestInfo} from 'relay-runtime/store/RelayStoreTypes';
26
+
27
+ const useRelayEnvironment = require('../useRelayEnvironment');
28
+ const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
29
+ const invariant = require('invariant');
30
+ const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
31
+ const {
32
+ areEqualSelectors,
33
+ createOperationDescriptor,
34
+ getPendingOperationsForFragment,
35
+ getSelector,
36
+ getVariablesFromFragment,
37
+ handlePotentialSnapshotErrors,
38
+ recycleNodesInto,
39
+ } = require('relay-runtime');
40
+
41
+ type FragmentQueryOptions = {|
42
+ fetchPolicy?: FetchPolicy,
43
+ networkCacheConfig?: CacheConfig,
44
+ |};
45
+
46
+ type FragmentState = $ReadOnly<
47
+ | {|kind: 'bailout'|}
48
+ | {|kind: 'singular', snapshot: Snapshot, epoch: number|}
49
+ | {|kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number|},
50
+ >;
51
+
52
+ type StateUpdater<T> = (T | (T => T)) => void;
53
+
54
+ function isMissingData(state: FragmentState): boolean {
55
+ if (state.kind === 'bailout') {
56
+ return false;
57
+ } else if (state.kind === 'singular') {
58
+ return state.snapshot.isMissingData;
59
+ } else {
60
+ return state.snapshots.some(s => s.isMissingData);
61
+ }
62
+ }
63
+
64
+ function getMissingClientEdges(
65
+ state: FragmentState,
66
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
67
+ if (state.kind === 'bailout') {
68
+ return null;
69
+ } else if (state.kind === 'singular') {
70
+ return state.snapshot.missingClientEdges ?? null;
71
+ } else {
72
+ let edges = null;
73
+ for (const snapshot of state.snapshots) {
74
+ if (snapshot.missingClientEdges) {
75
+ edges = edges ?? [];
76
+ for (const edge of snapshot.missingClientEdges) {
77
+ edges.push(edge);
78
+ }
79
+ }
80
+ }
81
+ return edges;
82
+ }
83
+ }
84
+
85
+ function handlePotentialSnapshotErrorsForState(
86
+ environment: IEnvironment,
87
+ state: FragmentState,
88
+ ): void {
89
+ if (state.kind === 'singular') {
90
+ handlePotentialSnapshotErrors(
91
+ environment,
92
+ state.snapshot.missingRequiredFields,
93
+ state.snapshot.relayResolverErrors,
94
+ );
95
+ } else if (state.kind === 'plural') {
96
+ for (const snapshot of state.snapshots) {
97
+ handlePotentialSnapshotErrors(
98
+ environment,
99
+ snapshot.missingRequiredFields,
100
+ snapshot.relayResolverErrors,
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ function handleMissedUpdates(
107
+ environment: IEnvironment,
108
+ state: FragmentState,
109
+ setState: StateUpdater<FragmentState>,
110
+ ): void {
111
+ if (state.kind === 'bailout') {
112
+ return;
113
+ }
114
+ const currentEpoch = environment.getStore().getEpoch();
115
+ if (currentEpoch === state.epoch) {
116
+ return;
117
+ }
118
+ // The store has updated since we rendered (without us being subscribed yet),
119
+ // so check for any updates to the data we're rendering:
120
+ if (state.kind === 'singular') {
121
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
122
+ const updatedData = recycleNodesInto(
123
+ state.snapshot.data,
124
+ currentSnapshot.data,
125
+ );
126
+ if (updatedData !== state.snapshot.data) {
127
+ setState({
128
+ kind: 'singular',
129
+ snapshot: currentSnapshot,
130
+ epoch: currentEpoch,
131
+ });
132
+ }
133
+ } else {
134
+ let updates = null;
135
+ for (let index = 0; index < state.snapshots.length; index++) {
136
+ const snapshot = state.snapshots[index];
137
+ const currentSnapshot = environment.lookup(snapshot.selector);
138
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
139
+ if (updatedData !== snapshot.data) {
140
+ updates =
141
+ updates === null ? new Array(state.snapshots.length) : updates;
142
+ updates[index] = snapshot;
143
+ }
144
+ }
145
+ if (updates !== null) {
146
+ const theUpdates = updates; // preserve flow refinement.
147
+ setState(existing => {
148
+ invariant(
149
+ existing.kind === 'plural',
150
+ 'Cannot go from singular to plural or from bailout to plural.',
151
+ );
152
+ const updated = [...existing.snapshots];
153
+ for (let index = 0; index < theUpdates.length; index++) {
154
+ const updatedSnapshot = theUpdates[index];
155
+ if (updatedSnapshot) {
156
+ updated[index] = updatedSnapshot;
157
+ }
158
+ }
159
+ return {kind: 'plural', snapshots: updated, epoch: currentEpoch};
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ function handleMissingClientEdge(
166
+ environment: IEnvironment,
167
+ parentFragmentNode: ReaderFragment,
168
+ parentFragmentRef: mixed,
169
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
170
+ queryOptions?: FragmentQueryOptions,
171
+ ): void {
172
+ const originalVariables = getVariablesFromFragment(
173
+ parentFragmentNode,
174
+ parentFragmentRef,
175
+ );
176
+ const variables = {
177
+ ...originalVariables,
178
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
179
+ };
180
+ const queryOperationDescriptor = createOperationDescriptor(
181
+ missingClientEdgeRequestInfo.request,
182
+ variables,
183
+ queryOptions?.networkCacheConfig,
184
+ );
185
+ // This may suspend. We don't need to do anything with the results; all we're
186
+ // doing here is started the query if needed and retaining and releasing it
187
+ // according to the component mount/suspense cycle; getQueryResultOrFetchQuery
188
+ // already handles this by itself.
189
+ getQueryResultOrFetchQuery(
190
+ environment,
191
+ queryOperationDescriptor,
192
+ queryOptions?.fetchPolicy,
193
+ );
194
+ }
195
+
196
+ function subscribeToSnapshot(
197
+ environment: IEnvironment,
198
+ state: FragmentState,
199
+ setState: StateUpdater<FragmentState>,
200
+ ): () => void {
201
+ if (state.kind === 'bailout') {
202
+ return () => {};
203
+ } else if (state.kind === 'singular') {
204
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
205
+ setState({
206
+ kind: 'singular',
207
+ snapshot: latestSnapshot,
208
+ epoch: environment.getStore().getEpoch(),
209
+ });
210
+ });
211
+ return () => {
212
+ disposable.dispose();
213
+ };
214
+ } else {
215
+ const disposables = state.snapshots.map((snapshot, index) =>
216
+ environment.subscribe(snapshot, latestSnapshot => {
217
+ setState(existing => {
218
+ invariant(
219
+ existing.kind === 'plural',
220
+ 'Cannot go from singular to plural or from bailout to plural.',
221
+ );
222
+ const updated = [...existing.snapshots];
223
+ updated[index] = latestSnapshot;
224
+ return {
225
+ kind: 'plural',
226
+ snapshots: updated,
227
+ epoch: environment.getStore().getEpoch(),
228
+ };
229
+ });
230
+ }),
231
+ );
232
+ return () => {
233
+ for (const d of disposables) {
234
+ d.dispose();
235
+ }
236
+ };
237
+ }
238
+ }
239
+
240
+ function getFragmentState(
241
+ environment: IEnvironment,
242
+ fragmentSelector: ?ReaderSelector,
243
+ ): FragmentState {
244
+ if (fragmentSelector == null) {
245
+ return {kind: 'bailout'};
246
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
247
+ return {
248
+ kind: 'plural',
249
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
250
+ epoch: environment.getStore().getEpoch(),
251
+ };
252
+ } else {
253
+ return {
254
+ kind: 'singular',
255
+ snapshot: environment.lookup(fragmentSelector),
256
+ epoch: environment.getStore().getEpoch(),
257
+ };
258
+ }
259
+ }
260
+
261
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
262
+ function useFragmentInternal_REACT_CACHE(
263
+ fragmentNode: ReaderFragment,
264
+ fragmentRef: mixed,
265
+ hookDisplayName: string,
266
+ queryOptions?: FragmentQueryOptions,
267
+ fragmentKey?: string,
268
+ ): {|
269
+ data: ?SelectorData | Array<?SelectorData>,
270
+ disableStoreUpdates: () => void,
271
+ enableStoreUpdates: () => void,
272
+ |} {
273
+ const fragmentSelector = getSelector(fragmentNode, fragmentRef);
274
+
275
+ if (fragmentNode?.metadata?.plural === true) {
276
+ invariant(
277
+ Array.isArray(fragmentRef),
278
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
279
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
280
+ 'from fragment `%s` to allow the prop to be an object.',
281
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
282
+ fragmentNode.name,
283
+ typeof fragmentRef,
284
+ fragmentNode.name,
285
+ );
286
+ } else {
287
+ invariant(
288
+ !Array.isArray(fragmentRef),
289
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
290
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
291
+ 'to fragment `%s` to allow the prop to be an array.',
292
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
293
+ fragmentNode.name,
294
+ typeof fragmentRef,
295
+ fragmentNode.name,
296
+ );
297
+ }
298
+ invariant(
299
+ fragmentRef == null || fragmentSelector != null,
300
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
301
+ 'but the fragment reference was not found`. This is most ' +
302
+ 'likely the result of:\n' +
303
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
304
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
305
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
306
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
307
+ 'spread - then the fragment reference will not exist. ' +
308
+ 'In this case, pass `null` if the conditions for evaluating the ' +
309
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
310
+ fragmentNode.name,
311
+ fragmentNode.name,
312
+ hookDisplayName,
313
+ fragmentNode.name,
314
+ fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
315
+ hookDisplayName,
316
+ );
317
+
318
+ const environment = useRelayEnvironment();
319
+ const [rawState, setState] = useState<FragmentState>(() =>
320
+ getFragmentState(environment, fragmentSelector),
321
+ );
322
+
323
+ const [previousFragmentSelector, setPreviousFragmentSelector] =
324
+ useState(fragmentSelector);
325
+ if (!areEqualSelectors(fragmentSelector, previousFragmentSelector)) {
326
+ setPreviousFragmentSelector(fragmentSelector);
327
+ setState(getFragmentState(environment, fragmentSelector));
328
+ }
329
+
330
+ let state;
331
+ if (fragmentRef == null) {
332
+ state = {kind: 'bailout'};
333
+ } else if (rawState.kind === 'plural' && rawState.snapshots.length === 0) {
334
+ state = {kind: 'bailout'};
335
+ } else {
336
+ state = rawState;
337
+ }
338
+
339
+ // Handle the queries for any missing client edges; this may suspend.
340
+ // FIXME handle client edges in parallel.
341
+ const missingClientEdges = getMissingClientEdges(state);
342
+ if (missingClientEdges?.length) {
343
+ for (const edge of missingClientEdges) {
344
+ handleMissingClientEdge(
345
+ environment,
346
+ fragmentNode,
347
+ fragmentRef,
348
+ edge,
349
+ queryOptions,
350
+ );
351
+ }
352
+ }
353
+
354
+ if (isMissingData(state)) {
355
+ // Suspend if an active operation bears on this fragment, either the
356
+ // fragment's owner or some other mutation etc. that could affect it:
357
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
358
+ const fragmentOwner =
359
+ fragmentSelector.kind === 'PluralReaderSelector'
360
+ ? fragmentSelector.selectors[0].owner
361
+ : fragmentSelector.owner;
362
+ const pendingOperationsResult = getPendingOperationsForFragment(
363
+ environment,
364
+ fragmentNode,
365
+ fragmentOwner,
366
+ );
367
+ if (pendingOperationsResult) {
368
+ throw pendingOperationsResult.promise;
369
+ }
370
+ // Report required fields only if we're not suspending, since that means
371
+ // they're missing even though we are out of options for possibly fetching them:
372
+ handlePotentialSnapshotErrorsForState(environment, state);
373
+ }
374
+
375
+ // Subscriptions:
376
+ const isMountedRef = useRef(false);
377
+ const isListeningForUpdatesRef = useRef(true);
378
+ function enableStoreUpdates() {
379
+ isListeningForUpdatesRef.current = true;
380
+ handleMissedUpdates(environment, state, setState);
381
+ }
382
+ function disableStoreUpdates() {
383
+ isListeningForUpdatesRef.current = false;
384
+ }
385
+ useEffect(() => {
386
+ const wasAlreadySubscribed = isMountedRef.current;
387
+ isMountedRef.current = true;
388
+ if (!wasAlreadySubscribed) {
389
+ handleMissedUpdates(environment, state, setState);
390
+ }
391
+ return subscribeToSnapshot(environment, state, setState);
392
+ }, [environment, state]);
393
+
394
+ const data = useMemo(
395
+ () =>
396
+ state.kind === 'bailout'
397
+ ? {}
398
+ : state.kind === 'singular'
399
+ ? state.snapshot.data
400
+ : state.snapshots.map(s => s.data),
401
+ [state],
402
+ );
403
+
404
+ if (__DEV__) {
405
+ // eslint-disable-next-line react-hooks/rules-of-hooks
406
+ useDebugValue({fragment: fragmentNode.name, data});
407
+ }
408
+
409
+ return {
410
+ data,
411
+ disableStoreUpdates,
412
+ enableStoreUpdates,
413
+ };
414
+ }
415
+
416
+ module.exports = useFragmentInternal_REACT_CACHE;
@@ -0,0 +1,66 @@
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
+ // flowlint ambiguous-object-type:error
13
+
14
+ 'use strict';
15
+
16
+ import type {
17
+ CacheConfig,
18
+ FetchPolicy,
19
+ Query,
20
+ RenderPolicy,
21
+ Variables,
22
+ } from 'relay-runtime';
23
+
24
+ const {useTrackLoadQueryInRender} = require('../loadQuery');
25
+ const useMemoOperationDescriptor = require('../useMemoOperationDescriptor');
26
+ const useRelayEnvironment = require('../useRelayEnvironment');
27
+ const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
28
+ const useFragmentInternal = require('./useFragmentInternal_REACT_CACHE');
29
+
30
+ function useLazyLoadQuery_REACT_CACHE<TVariables: Variables, TData>(
31
+ gqlQuery: Query<TVariables, TData>,
32
+ variables: TVariables,
33
+ options?: {|
34
+ fetchKey?: string | number,
35
+ fetchPolicy?: FetchPolicy,
36
+ networkCacheConfig?: CacheConfig,
37
+ UNSTABLE_renderPolicy?: RenderPolicy,
38
+ |},
39
+ ): TData {
40
+ useTrackLoadQueryInRender();
41
+ const environment = useRelayEnvironment();
42
+
43
+ const queryOperationDescriptor = useMemoOperationDescriptor(
44
+ gqlQuery,
45
+ variables,
46
+ options?.networkCacheConfig ?? {force: true},
47
+ );
48
+
49
+ // Get the query going if needed -- this may suspend.
50
+ const queryResult = getQueryResultOrFetchQuery(
51
+ environment,
52
+ queryOperationDescriptor,
53
+ options?.fetchPolicy,
54
+ );
55
+
56
+ // Read the query's root fragment -- this may suspend.
57
+ const {fragmentNode, fragmentRef} = queryResult;
58
+
59
+ // $FlowExpectedError[incompatible-return] Is this a fixable incompatible-return?
60
+ return useFragmentInternal(fragmentNode, fragmentRef, 'useLazyLoadQuery()', {
61
+ fetchPolicy: options?.fetchPolicy,
62
+ networkCacheConfig: options?.networkCacheConfig,
63
+ }).data;
64
+ }
65
+
66
+ module.exports = useLazyLoadQuery_REACT_CACHE;