react-relay 13.1.0 → 14.0.0

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