react-relay 13.1.0 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +7 -4
  3. package/ReactRelayLocalQueryRenderer.js.flow +1 -1
  4. package/ReactRelayPaginationContainer.js.flow +13 -8
  5. package/ReactRelayQueryFetcher.js.flow +1 -0
  6. package/ReactRelayQueryRenderer.js.flow +7 -6
  7. package/ReactRelayRefetchContainer.js.flow +10 -3
  8. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +2 -2
  9. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +2 -2
  10. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +3 -3
  11. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -3
  12. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +3 -3
  13. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -3
  14. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +2 -2
  15. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +2 -2
  16. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +2 -2
  17. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +2 -2
  18. package/buildReactRelayContainer.js.flow +2 -2
  19. package/hooks.js +1 -1
  20. package/index.js +1 -1
  21. package/jest-react/internalAct.js.flow +25 -9
  22. package/legacy.js +1 -1
  23. package/lib/ReactRelayQueryFetcher.js +1 -0
  24. package/lib/ReactRelayQueryRenderer.js +1 -2
  25. package/lib/jest-react/internalAct.js +24 -4
  26. package/lib/readContext.js +2 -1
  27. package/lib/relay-hooks/FragmentResource.js +62 -23
  28. package/lib/relay-hooks/HooksImplementation.js +29 -0
  29. package/lib/relay-hooks/MatchContainer.js +1 -0
  30. package/lib/relay-hooks/QueryResource.js +4 -166
  31. package/lib/relay-hooks/preloadQuery_DEPRECATED.js +7 -11
  32. package/lib/relay-hooks/react-cache/RelayReactCache.js +37 -0
  33. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +344 -0
  34. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +540 -0
  35. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +51 -0
  36. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +56 -0
  37. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +125 -0
  38. package/lib/relay-hooks/useFragment.js +15 -1
  39. package/lib/relay-hooks/useLazyLoadQuery.js +18 -2
  40. package/lib/relay-hooks/useMutation.js +4 -5
  41. package/lib/relay-hooks/usePreloadedQuery.js +18 -2
  42. package/package.json +3 -3
  43. package/react-relay-hooks.js +2 -2
  44. package/react-relay-hooks.min.js +2 -2
  45. package/react-relay-legacy.js +2 -2
  46. package/react-relay-legacy.min.js +2 -2
  47. package/react-relay.js +2 -2
  48. package/react-relay.min.js +2 -2
  49. package/readContext.js.flow +1 -0
  50. package/relay-hooks/FragmentResource.js.flow +72 -27
  51. package/relay-hooks/HooksImplementation.js.flow +45 -0
  52. package/relay-hooks/MatchContainer.js.flow +8 -1
  53. package/relay-hooks/QueryResource.js.flow +8 -203
  54. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +2 -2
  55. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +2 -2
  56. package/relay-hooks/loadQuery.js.flow +2 -1
  57. package/relay-hooks/preloadQuery_DEPRECATED.js.flow +7 -14
  58. package/relay-hooks/react-cache/RelayReactCache.js.flow +42 -0
  59. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +424 -0
  60. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +559 -0
  61. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +74 -0
  62. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +72 -0
  63. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +153 -0
  64. package/relay-hooks/useFragment.js.flow +17 -10
  65. package/relay-hooks/useLazyLoadQuery.js.flow +38 -3
  66. package/relay-hooks/useMutation.js.flow +3 -3
  67. package/relay-hooks/usePreloadedQuery.js.flow +30 -2
  68. package/relay-hooks/useRefetchableFragmentNode.js.flow +26 -11
  69. package/relay-hooks/useSubscription.js.flow +14 -8
@@ -0,0 +1,559 @@
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
+ const warning = require('warning');
41
+
42
+ type FragmentQueryOptions = {|
43
+ fetchPolicy?: FetchPolicy,
44
+ networkCacheConfig?: ?CacheConfig,
45
+ |};
46
+
47
+ type FragmentState = $ReadOnly<
48
+ | {|kind: 'bailout'|}
49
+ | {|kind: 'singular', snapshot: Snapshot, epoch: number|}
50
+ | {|kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number|},
51
+ >;
52
+
53
+ type StateUpdater<T> = (T | (T => T)) => void;
54
+ type StateUpdaterFunction<T> = ((T) => T) => void;
55
+
56
+ function isMissingData(state: FragmentState): boolean {
57
+ if (state.kind === 'bailout') {
58
+ return false;
59
+ } else if (state.kind === 'singular') {
60
+ return state.snapshot.isMissingData;
61
+ } else {
62
+ return state.snapshots.some(s => s.isMissingData);
63
+ }
64
+ }
65
+
66
+ function getMissingClientEdges(
67
+ state: FragmentState,
68
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
69
+ if (state.kind === 'bailout') {
70
+ return null;
71
+ } else if (state.kind === 'singular') {
72
+ return state.snapshot.missingClientEdges ?? null;
73
+ } else {
74
+ let edges = null;
75
+ for (const snapshot of state.snapshots) {
76
+ if (snapshot.missingClientEdges) {
77
+ edges = edges ?? [];
78
+ for (const edge of snapshot.missingClientEdges) {
79
+ edges.push(edge);
80
+ }
81
+ }
82
+ }
83
+ return edges;
84
+ }
85
+ }
86
+
87
+ function handlePotentialSnapshotErrorsForState(
88
+ environment: IEnvironment,
89
+ state: FragmentState,
90
+ ): void {
91
+ if (state.kind === 'singular') {
92
+ handlePotentialSnapshotErrors(
93
+ environment,
94
+ state.snapshot.missingRequiredFields,
95
+ state.snapshot.relayResolverErrors,
96
+ );
97
+ } else if (state.kind === 'plural') {
98
+ for (const snapshot of state.snapshots) {
99
+ handlePotentialSnapshotErrors(
100
+ environment,
101
+ snapshot.missingRequiredFields,
102
+ snapshot.relayResolverErrors,
103
+ );
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check for updates to the store that occurred concurrently with rendering the given `state` value,
110
+ * returning a new (updated) state if there were updates or null if there were no changes.
111
+ */
112
+ function handleMissedUpdates(
113
+ environment: IEnvironment,
114
+ state: FragmentState,
115
+ ): null | [/* has data changed */ boolean, FragmentState] {
116
+ if (state.kind === 'bailout') {
117
+ return null;
118
+ }
119
+ // FIXME this is invalid if we've just switched environments.
120
+ const currentEpoch = environment.getStore().getEpoch();
121
+ if (currentEpoch === state.epoch) {
122
+ return null;
123
+ }
124
+ // The store has updated since we rendered (without us being subscribed yet),
125
+ // so check for any updates to the data we're rendering:
126
+ if (state.kind === 'singular') {
127
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
128
+ const updatedData = recycleNodesInto(
129
+ state.snapshot.data,
130
+ currentSnapshot.data,
131
+ );
132
+ const updatedCurrentSnapshot: Snapshot = {
133
+ data: updatedData,
134
+ isMissingData: currentSnapshot.isMissingData,
135
+ missingClientEdges: currentSnapshot.missingClientEdges,
136
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
137
+ seenRecords: currentSnapshot.seenRecords,
138
+ selector: currentSnapshot.selector,
139
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
140
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
141
+ };
142
+ return [
143
+ updatedData !== state.snapshot.data,
144
+ {
145
+ kind: 'singular',
146
+ snapshot: updatedCurrentSnapshot,
147
+ epoch: currentEpoch,
148
+ },
149
+ ];
150
+ } else {
151
+ let didMissUpdates = false;
152
+ const currentSnapshots = [];
153
+ for (let index = 0; index < state.snapshots.length; index++) {
154
+ const snapshot = state.snapshots[index];
155
+ const currentSnapshot = environment.lookup(snapshot.selector);
156
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
157
+ const updatedCurrentSnapshot: Snapshot = {
158
+ data: updatedData,
159
+ isMissingData: currentSnapshot.isMissingData,
160
+ missingClientEdges: currentSnapshot.missingClientEdges,
161
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
162
+ seenRecords: currentSnapshot.seenRecords,
163
+ selector: currentSnapshot.selector,
164
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
165
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
166
+ };
167
+ if (updatedData !== snapshot.data) {
168
+ didMissUpdates = true;
169
+ }
170
+ currentSnapshots.push(updatedCurrentSnapshot);
171
+ }
172
+ invariant(
173
+ currentSnapshots.length === state.snapshots.length,
174
+ 'Expected same number of snapshots',
175
+ );
176
+ return [
177
+ didMissUpdates,
178
+ {
179
+ kind: 'plural',
180
+ snapshots: currentSnapshots,
181
+ epoch: currentEpoch,
182
+ },
183
+ ];
184
+ }
185
+ }
186
+
187
+ function handleMissingClientEdge(
188
+ environment: IEnvironment,
189
+ parentFragmentNode: ReaderFragment,
190
+ parentFragmentRef: mixed,
191
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
192
+ queryOptions?: FragmentQueryOptions,
193
+ ): () => () => void {
194
+ const originalVariables = getVariablesFromFragment(
195
+ parentFragmentNode,
196
+ parentFragmentRef,
197
+ );
198
+ const variables = {
199
+ ...originalVariables,
200
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
201
+ };
202
+ const queryOperationDescriptor = createOperationDescriptor(
203
+ missingClientEdgeRequestInfo.request,
204
+ variables,
205
+ queryOptions?.networkCacheConfig,
206
+ );
207
+ // This may suspend. We don't need to do anything with the results; all we're
208
+ // doing here is started the query if needed and retaining and releasing it
209
+ // according to the component mount/suspense cycle; getQueryResultOrFetchQuery
210
+ // already handles this by itself.
211
+ const [_, effect] = getQueryResultOrFetchQuery(
212
+ environment,
213
+ queryOperationDescriptor,
214
+ {
215
+ fetchPolicy: queryOptions?.fetchPolicy,
216
+ },
217
+ );
218
+ return effect;
219
+ }
220
+
221
+ function subscribeToSnapshot(
222
+ environment: IEnvironment,
223
+ state: FragmentState,
224
+ setState: StateUpdaterFunction<FragmentState>,
225
+ ): () => void {
226
+ if (state.kind === 'bailout') {
227
+ return () => {};
228
+ } else if (state.kind === 'singular') {
229
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
230
+ setState(_ => ({
231
+ kind: 'singular',
232
+ snapshot: latestSnapshot,
233
+ epoch: environment.getStore().getEpoch(),
234
+ }));
235
+ });
236
+ return () => {
237
+ disposable.dispose();
238
+ };
239
+ } else {
240
+ const disposables = state.snapshots.map((snapshot, index) =>
241
+ 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];
248
+ updated[index] = latestSnapshot;
249
+ return {
250
+ kind: 'plural',
251
+ snapshots: updated,
252
+ epoch: environment.getStore().getEpoch(),
253
+ };
254
+ });
255
+ }),
256
+ );
257
+ return () => {
258
+ for (const d of disposables) {
259
+ d.dispose();
260
+ }
261
+ };
262
+ }
263
+ }
264
+
265
+ function getFragmentState(
266
+ environment: IEnvironment,
267
+ fragmentSelector: ?ReaderSelector,
268
+ isPlural: boolean,
269
+ ): FragmentState {
270
+ if (fragmentSelector == null) {
271
+ return {kind: 'bailout'};
272
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
273
+ return {
274
+ kind: 'plural',
275
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
276
+ epoch: environment.getStore().getEpoch(),
277
+ };
278
+ } else {
279
+ return {
280
+ kind: 'singular',
281
+ snapshot: environment.lookup(fragmentSelector),
282
+ epoch: environment.getStore().getEpoch(),
283
+ };
284
+ }
285
+ }
286
+
287
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
288
+ function useFragmentInternal_REACT_CACHE(
289
+ fragmentNode: ReaderFragment,
290
+ fragmentRef: mixed,
291
+ hookDisplayName: string,
292
+ queryOptions?: FragmentQueryOptions,
293
+ fragmentKey?: string,
294
+ ): ?SelectorData | Array<?SelectorData> {
295
+ const fragmentSelector = useMemo(
296
+ () => getSelector(fragmentNode, fragmentRef),
297
+ [fragmentNode, fragmentRef],
298
+ );
299
+
300
+ const isPlural = fragmentNode?.metadata?.plural === true;
301
+
302
+ if (isPlural) {
303
+ invariant(
304
+ fragmentRef == null || Array.isArray(fragmentRef),
305
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
306
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
307
+ 'from fragment `%s` to allow the prop to be an object.',
308
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
309
+ fragmentNode.name,
310
+ typeof fragmentRef,
311
+ fragmentNode.name,
312
+ );
313
+ } else {
314
+ invariant(
315
+ !Array.isArray(fragmentRef),
316
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
317
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
318
+ 'to fragment `%s` to allow the prop to be an array.',
319
+ fragmentKey != null ? ` for key \`${fragmentKey}\`` : '',
320
+ fragmentNode.name,
321
+ typeof fragmentRef,
322
+ fragmentNode.name,
323
+ );
324
+ }
325
+ invariant(
326
+ fragmentRef == null ||
327
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
328
+ fragmentSelector != null,
329
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
330
+ 'but the fragment reference was not found`. This is most ' +
331
+ 'likely the result of:\n' +
332
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
333
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
334
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
335
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
336
+ 'spread - then the fragment reference will not exist. ' +
337
+ 'In this case, pass `null` if the conditions for evaluating the ' +
338
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
339
+ fragmentNode.name,
340
+ fragmentNode.name,
341
+ hookDisplayName,
342
+ fragmentNode.name,
343
+ fragmentKey == null ? 'a fragment reference' : `the \`${fragmentKey}\``,
344
+ hookDisplayName,
345
+ );
346
+
347
+ const environment = useRelayEnvironment();
348
+ const [rawState, setState] = useState<FragmentState>(() =>
349
+ getFragmentState(environment, fragmentSelector, isPlural),
350
+ );
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);
364
+
365
+ // This copy of the state we only update when something requires us to
366
+ // unsubscribe and re-subscribe, namely a changed environment or
367
+ // fragment selector.
368
+ const [rawSubscribedState, setSubscribedState] = useState(state);
369
+ // FIXME since this is used as an effect dependency, it needs to be memoized.
370
+ let subscribedState = stateFromRawState(rawSubscribedState);
371
+
372
+ const [previousFragmentSelector, setPreviousFragmentSelector] =
373
+ useState(fragmentSelector);
374
+ const [previousEnvironment, setPreviousEnvironment] = useState(environment);
375
+ if (
376
+ !areEqualSelectors(fragmentSelector, previousFragmentSelector) ||
377
+ environment !== previousEnvironment
378
+ ) {
379
+ // Enqueue setState to record the new selector and state
380
+ setPreviousFragmentSelector(fragmentSelector);
381
+ setPreviousEnvironment(environment);
382
+ const newState = stateFromRawState(
383
+ getFragmentState(environment, fragmentSelector, isPlural),
384
+ );
385
+ setState(newState);
386
+ setSubscribedState(newState); // This causes us to form a new subscription
387
+ // But render with the latest state w/o waiting for the setState. Otherwise
388
+ // the component would render the wrong information temporarily (including
389
+ // possibly incorrectly triggering some warnings below).
390
+ state = newState;
391
+ subscribedState = newState;
392
+ }
393
+
394
+ // Handle the queries for any missing client edges; this may suspend.
395
+ // FIXME handle client edges in parallel.
396
+ if (fragmentNode.metadata?.hasClientEdges === true) {
397
+ // The fragment is validated to be static (in useFragment) and hasClientEdges is
398
+ // a static (constant) property of the fragment. In practice, this effect will
399
+ // always or never run for a given invocation of this hook.
400
+ // eslint-disable-next-line react-hooks/rules-of-hooks
401
+ const effects = useMemo(() => {
402
+ const missingClientEdges = getMissingClientEdges(state);
403
+ // eslint-disable-next-line no-shadow
404
+ let effects;
405
+ if (missingClientEdges?.length) {
406
+ effects = [];
407
+ for (const edge of missingClientEdges) {
408
+ effects.push(
409
+ handleMissingClientEdge(
410
+ environment,
411
+ fragmentNode,
412
+ fragmentRef,
413
+ edge,
414
+ queryOptions,
415
+ ),
416
+ );
417
+ }
418
+ }
419
+ return effects;
420
+ }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
421
+
422
+ // See above note
423
+ // eslint-disable-next-line react-hooks/rules-of-hooks
424
+ useEffect(() => {
425
+ if (effects?.length) {
426
+ const cleanups = [];
427
+ for (const effect of effects) {
428
+ cleanups.push(effect());
429
+ }
430
+ return () => {
431
+ for (const cleanup of cleanups) {
432
+ cleanup();
433
+ }
434
+ };
435
+ }
436
+ }, [effects]);
437
+ }
438
+
439
+ if (isMissingData(state)) {
440
+ // 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;
454
+ }
455
+ // Report required fields only if we're not suspending, since that means
456
+ // they're missing even though we are out of options for possibly fetching them:
457
+ handlePotentialSnapshotErrorsForState(environment, state);
458
+ }
459
+
460
+ useEffect(() => {
461
+ // Check for updates since the state was rendered
462
+ let currentState = subscribedState;
463
+ const updates = handleMissedUpdates(environment, subscribedState);
464
+ if (updates !== null) {
465
+ const [didMissUpdates, updatedState] = updates;
466
+ // TODO: didMissUpdates only checks for changes to snapshot data, but it's possible
467
+ // that other snapshot properties may have changed that should also trigger a re-render,
468
+ // such as changed missing resolver fields, missing client edges, etc.
469
+ // A potential alternative is for handleMissedUpdates() to recycle the entire state
470
+ // value, and return the new (recycled) state only if there was some change. In that
471
+ // case the code would always setState if something in the snapshot changed, in addition
472
+ // to using the latest snapshot to subscribe.
473
+ if (didMissUpdates) {
474
+ setState(updatedState);
475
+ }
476
+ currentState = updatedState;
477
+ }
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
+ });
495
+ }, [environment, subscribedState]);
496
+
497
+ let data: ?SelectorData | Array<?SelectorData>;
498
+ if (isPlural) {
499
+ // Plural fragments require allocating an array of the snasphot data values,
500
+ // which has to be memoized to avoid triggering downstream re-renders.
501
+ //
502
+ // Note that isPlural is a constant property of the fragment and does not change
503
+ // for a particular useFragment invocation site
504
+ // eslint-disable-next-line react-hooks/rules-of-hooks
505
+ data = useMemo(() => {
506
+ if (state.kind === 'bailout') {
507
+ return [];
508
+ } else {
509
+ invariant(
510
+ state.kind === 'plural',
511
+ 'Expected state to be plural because fragment is plural',
512
+ );
513
+ return state.snapshots.map(s => s.data);
514
+ }
515
+ }, [state]);
516
+ } else if (state.kind === 'bailout') {
517
+ // This case doesn't allocate a new object so it doesn't have to be memoized
518
+ data = null;
519
+ } else {
520
+ // This case doesn't allocate a new object so it doesn't have to be memoized
521
+ invariant(
522
+ state.kind === 'singular',
523
+ 'Expected state to be singular because fragment is singular',
524
+ );
525
+ data = state.snapshot.data;
526
+ }
527
+
528
+ if (__DEV__) {
529
+ if (
530
+ fragmentRef != null &&
531
+ (data === undefined ||
532
+ (Array.isArray(data) &&
533
+ data.length > 0 &&
534
+ data.every(d => d === undefined)))
535
+ ) {
536
+ warning(
537
+ false,
538
+ 'Relay: Expected to have been able to read non-null data for ' +
539
+ 'fragment `%s` declared in ' +
540
+ '`%s`, since fragment reference was non-null. ' +
541
+ "Make sure that that `%s`'s parent isn't " +
542
+ 'holding on to and/or passing a fragment reference for data that ' +
543
+ 'has been deleted.',
544
+ fragmentNode.name,
545
+ hookDisplayName,
546
+ hookDisplayName,
547
+ );
548
+ }
549
+ }
550
+
551
+ if (__DEV__) {
552
+ // eslint-disable-next-line react-hooks/rules-of-hooks
553
+ useDebugValue({fragment: fragmentNode.name, data});
554
+ }
555
+
556
+ return data;
557
+ }
558
+
559
+ module.exports = useFragmentInternal_REACT_CACHE;
@@ -0,0 +1,74 @@
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
+ // flowlint ambiguous-object-type:error
13
+
14
+ 'use strict';
15
+
16
+ import type {Fragment, FragmentType, GraphQLTaggedNode} from 'relay-runtime';
17
+
18
+ const {useTrackLoadQueryInRender} = require('../loadQuery');
19
+ const useStaticFragmentNodeWarning = require('../useStaticFragmentNodeWarning');
20
+ const useFragmentInternal = require('./useFragmentInternal_REACT_CACHE');
21
+ const {useDebugValue} = require('react');
22
+ const {getFragment} = require('relay-runtime');
23
+
24
+ type HasSpread<TFragmentType> = {
25
+ +$fragmentSpreads: TFragmentType,
26
+ ...
27
+ };
28
+
29
+ // if the key is non-nullable, return non-nullable value
30
+ declare function useFragment<TFragmentType: FragmentType, TData>(
31
+ fragment: Fragment<TFragmentType, TData>,
32
+ key: HasSpread<TFragmentType>,
33
+ ): TData;
34
+
35
+ // if the key is nullable, return nullable value
36
+ declare function useFragment<TFragmentType: FragmentType, TData>(
37
+ fragment: Fragment<TFragmentType, TData>,
38
+ key: ?HasSpread<TFragmentType>,
39
+ ): ?TData;
40
+
41
+ // if the key is a non-nullable array of keys, return non-nullable array
42
+ declare function useFragment<TFragmentType: FragmentType, TData>(
43
+ fragment: Fragment<TFragmentType, TData>,
44
+ key: $ReadOnlyArray<HasSpread<TFragmentType>>,
45
+ ): TData;
46
+
47
+ // if the key is a nullable array of keys, return nullable array
48
+ declare function useFragment<TFragmentType: FragmentType, TData>(
49
+ fragment: Fragment<TFragmentType, TData>,
50
+ key: ?$ReadOnlyArray<HasSpread<TFragmentType>>,
51
+ ): ?TData;
52
+
53
+ function useFragment(fragment: GraphQLTaggedNode, key: mixed): mixed {
54
+ // We need to use this hook in order to be able to track if
55
+ // loadQuery was called during render
56
+ useTrackLoadQueryInRender();
57
+
58
+ const fragmentNode = getFragment(fragment);
59
+ if (__DEV__) {
60
+ // eslint-disable-next-line react-hooks/rules-of-hooks
61
+ useStaticFragmentNodeWarning(
62
+ fragmentNode,
63
+ 'first argument of useFragment()',
64
+ );
65
+ }
66
+ const data = useFragmentInternal(fragmentNode, key, 'useFragment()');
67
+ if (__DEV__) {
68
+ // eslint-disable-next-line react-hooks/rules-of-hooks
69
+ useDebugValue({fragment: fragmentNode.name, data});
70
+ }
71
+ return data;
72
+ }
73
+
74
+ module.exports = useFragment;
@@ -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
+ * @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
+ const {useEffect} = require('react');
30
+
31
+ function useLazyLoadQuery_REACT_CACHE<TVariables: Variables, TData>(
32
+ gqlQuery: Query<TVariables, TData>,
33
+ variables: TVariables,
34
+ options?: {|
35
+ fetchKey?: string | number,
36
+ fetchPolicy?: FetchPolicy,
37
+ networkCacheConfig?: CacheConfig,
38
+ UNSTABLE_renderPolicy?: RenderPolicy,
39
+ |},
40
+ ): TData {
41
+ useTrackLoadQueryInRender();
42
+ const environment = useRelayEnvironment();
43
+
44
+ const queryOperationDescriptor = useMemoOperationDescriptor(
45
+ gqlQuery,
46
+ variables,
47
+ options?.networkCacheConfig ?? {force: true},
48
+ );
49
+
50
+ // Get the query going if needed -- this may suspend.
51
+ const [queryResult, effect] = getQueryResultOrFetchQuery(
52
+ environment,
53
+ queryOperationDescriptor,
54
+ {
55
+ fetchPolicy: options?.fetchPolicy,
56
+ renderPolicy: options?.UNSTABLE_renderPolicy,
57
+ fetchKey: options?.fetchKey,
58
+ },
59
+ );
60
+
61
+ useEffect(effect);
62
+
63
+ // Read the query's root fragment -- this may suspend.
64
+ const {fragmentNode, fragmentRef} = queryResult;
65
+ // $FlowExpectedError[incompatible-return] Is this a fixable incompatible-return?
66
+ return useFragmentInternal(fragmentNode, fragmentRef, 'useLazyLoadQuery()', {
67
+ fetchPolicy: options?.fetchPolicy,
68
+ networkCacheConfig: options?.networkCacheConfig,
69
+ });
70
+ }
71
+
72
+ module.exports = useLazyLoadQuery_REACT_CACHE;