react-relay 13.2.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 (62) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +7 -4
  3. package/ReactRelayPaginationContainer.js.flow +13 -8
  4. package/ReactRelayQueryFetcher.js.flow +1 -0
  5. package/ReactRelayQueryRenderer.js.flow +6 -2
  6. package/ReactRelayRefetchContainer.js.flow +10 -3
  7. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer.graphql.js.flow +2 -2
  8. package/__flowtests__/__generated__/ReactRelayFragmentContainerFlowtest_viewer2.graphql.js.flow +2 -2
  9. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtestQuery.graphql.js.flow +3 -3
  10. package/__flowtests__/__generated__/ReactRelayPaginationContainerFlowtest_viewer.graphql.js.flow +3 -3
  11. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtestQuery.graphql.js.flow +3 -3
  12. package/__flowtests__/__generated__/ReactRelayRefetchContainerFlowtest_viewer.graphql.js.flow +3 -3
  13. package/__flowtests__/__generated__/RelayModernFlowtest_badref.graphql.js.flow +2 -2
  14. package/__flowtests__/__generated__/RelayModernFlowtest_notref.graphql.js.flow +2 -2
  15. package/__flowtests__/__generated__/RelayModernFlowtest_user.graphql.js.flow +2 -2
  16. package/__flowtests__/__generated__/RelayModernFlowtest_users.graphql.js.flow +2 -2
  17. package/buildReactRelayContainer.js.flow +2 -2
  18. package/hooks.js +1 -1
  19. package/index.js +1 -1
  20. package/legacy.js +1 -1
  21. package/lib/ReactRelayQueryFetcher.js +1 -0
  22. package/lib/ReactRelayQueryRenderer.js +0 -1
  23. package/lib/readContext.js +2 -1
  24. package/lib/relay-hooks/FragmentResource.js +52 -10
  25. package/lib/relay-hooks/HooksImplementation.js +29 -0
  26. package/lib/relay-hooks/MatchContainer.js +1 -0
  27. package/lib/relay-hooks/QueryResource.js +2 -1
  28. package/lib/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js +203 -56
  29. package/lib/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js +254 -109
  30. package/lib/relay-hooks/react-cache/useFragment_REACT_CACHE.js +51 -0
  31. package/lib/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js +13 -2
  32. package/lib/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js +125 -0
  33. package/lib/relay-hooks/useFragment.js +15 -1
  34. package/lib/relay-hooks/useLazyLoadQuery.js +18 -2
  35. package/lib/relay-hooks/useMutation.js +4 -5
  36. package/lib/relay-hooks/usePreloadedQuery.js +18 -2
  37. package/package.json +2 -2
  38. package/react-relay-hooks.js +2 -2
  39. package/react-relay-hooks.min.js +2 -2
  40. package/react-relay-legacy.js +2 -2
  41. package/react-relay-legacy.min.js +2 -2
  42. package/react-relay.js +2 -2
  43. package/react-relay.min.js +2 -2
  44. package/readContext.js.flow +1 -0
  45. package/relay-hooks/FragmentResource.js.flow +55 -9
  46. package/relay-hooks/HooksImplementation.js.flow +45 -0
  47. package/relay-hooks/MatchContainer.js.flow +8 -1
  48. package/relay-hooks/QueryResource.js.flow +4 -2
  49. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_user.graphql.js.flow +2 -2
  50. package/relay-hooks/__flowtests__/__generated__/useFragmentFlowtest_users.graphql.js.flow +2 -2
  51. package/relay-hooks/loadQuery.js.flow +2 -1
  52. package/relay-hooks/react-cache/getQueryResultOrFetchQuery_REACT_CACHE.js.flow +245 -64
  53. package/relay-hooks/react-cache/useFragmentInternal_REACT_CACHE.js.flow +242 -99
  54. package/relay-hooks/react-cache/useFragment_REACT_CACHE.js.flow +74 -0
  55. package/relay-hooks/react-cache/useLazyLoadQuery_REACT_CACHE.js.flow +10 -4
  56. package/relay-hooks/react-cache/usePreloadedQuery_REACT_CACHE.js.flow +153 -0
  57. package/relay-hooks/useFragment.js.flow +17 -10
  58. package/relay-hooks/useLazyLoadQuery.js.flow +38 -3
  59. package/relay-hooks/useMutation.js.flow +3 -3
  60. package/relay-hooks/usePreloadedQuery.js.flow +30 -2
  61. package/relay-hooks/useRefetchableFragmentNode.js.flow +26 -11
  62. package/relay-hooks/useSubscription.js.flow +14 -8
@@ -37,10 +37,11 @@ const {
37
37
  handlePotentialSnapshotErrors,
38
38
  recycleNodesInto,
39
39
  } = require('relay-runtime');
40
+ const warning = require('warning');
40
41
 
41
42
  type FragmentQueryOptions = {|
42
43
  fetchPolicy?: FetchPolicy,
43
- networkCacheConfig?: CacheConfig,
44
+ networkCacheConfig?: ?CacheConfig,
44
45
  |};
45
46
 
46
47
  type FragmentState = $ReadOnly<
@@ -50,6 +51,7 @@ type FragmentState = $ReadOnly<
50
51
  >;
51
52
 
52
53
  type StateUpdater<T> = (T | (T => T)) => void;
54
+ type StateUpdaterFunction<T> = ((T) => T) => void;
53
55
 
54
56
  function isMissingData(state: FragmentState): boolean {
55
57
  if (state.kind === 'bailout') {
@@ -103,17 +105,21 @@ function handlePotentialSnapshotErrorsForState(
103
105
  }
104
106
  }
105
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
+ */
106
112
  function handleMissedUpdates(
107
113
  environment: IEnvironment,
108
114
  state: FragmentState,
109
- setState: StateUpdater<FragmentState>,
110
- ): void {
115
+ ): null | [/* has data changed */ boolean, FragmentState] {
111
116
  if (state.kind === 'bailout') {
112
- return;
117
+ return null;
113
118
  }
119
+ // FIXME this is invalid if we've just switched environments.
114
120
  const currentEpoch = environment.getStore().getEpoch();
115
121
  if (currentEpoch === state.epoch) {
116
- return;
122
+ return null;
117
123
  }
118
124
  // The store has updated since we rendered (without us being subscribed yet),
119
125
  // so check for any updates to the data we're rendering:
@@ -123,42 +129,58 @@ function handleMissedUpdates(
123
129
  state.snapshot.data,
124
130
  currentSnapshot.data,
125
131
  );
126
- if (updatedData !== state.snapshot.data) {
127
- setState({
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
+ {
128
145
  kind: 'singular',
129
- snapshot: currentSnapshot,
146
+ snapshot: updatedCurrentSnapshot,
130
147
  epoch: currentEpoch,
131
- });
132
- }
148
+ },
149
+ ];
133
150
  } else {
134
- let updates = null;
151
+ let didMissUpdates = false;
152
+ const currentSnapshots = [];
135
153
  for (let index = 0; index < state.snapshots.length; index++) {
136
154
  const snapshot = state.snapshots[index];
137
155
  const currentSnapshot = environment.lookup(snapshot.selector);
138
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
+ };
139
167
  if (updatedData !== snapshot.data) {
140
- updates =
141
- updates === null ? new Array(state.snapshots.length) : updates;
142
- updates[index] = snapshot;
168
+ didMissUpdates = true;
143
169
  }
170
+ currentSnapshots.push(updatedCurrentSnapshot);
144
171
  }
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
- }
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
+ ];
162
184
  }
163
185
  }
164
186
 
@@ -168,7 +190,7 @@ function handleMissingClientEdge(
168
190
  parentFragmentRef: mixed,
169
191
  missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
170
192
  queryOptions?: FragmentQueryOptions,
171
- ): void {
193
+ ): () => () => void {
172
194
  const originalVariables = getVariablesFromFragment(
173
195
  parentFragmentNode,
174
196
  parentFragmentRef,
@@ -186,27 +208,30 @@ function handleMissingClientEdge(
186
208
  // doing here is started the query if needed and retaining and releasing it
187
209
  // according to the component mount/suspense cycle; getQueryResultOrFetchQuery
188
210
  // already handles this by itself.
189
- getQueryResultOrFetchQuery(
211
+ const [_, effect] = getQueryResultOrFetchQuery(
190
212
  environment,
191
213
  queryOperationDescriptor,
192
- queryOptions?.fetchPolicy,
214
+ {
215
+ fetchPolicy: queryOptions?.fetchPolicy,
216
+ },
193
217
  );
218
+ return effect;
194
219
  }
195
220
 
196
221
  function subscribeToSnapshot(
197
222
  environment: IEnvironment,
198
223
  state: FragmentState,
199
- setState: StateUpdater<FragmentState>,
224
+ setState: StateUpdaterFunction<FragmentState>,
200
225
  ): () => void {
201
226
  if (state.kind === 'bailout') {
202
227
  return () => {};
203
228
  } else if (state.kind === 'singular') {
204
229
  const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
205
- setState({
230
+ setState(_ => ({
206
231
  kind: 'singular',
207
232
  snapshot: latestSnapshot,
208
233
  epoch: environment.getStore().getEpoch(),
209
- });
234
+ }));
210
235
  });
211
236
  return () => {
212
237
  disposable.dispose();
@@ -240,6 +265,7 @@ function subscribeToSnapshot(
240
265
  function getFragmentState(
241
266
  environment: IEnvironment,
242
267
  fragmentSelector: ?ReaderSelector,
268
+ isPlural: boolean,
243
269
  ): FragmentState {
244
270
  if (fragmentSelector == null) {
245
271
  return {kind: 'bailout'};
@@ -265,16 +291,17 @@ function useFragmentInternal_REACT_CACHE(
265
291
  hookDisplayName: string,
266
292
  queryOptions?: FragmentQueryOptions,
267
293
  fragmentKey?: string,
268
- ): {|
269
- data: ?SelectorData | Array<?SelectorData>,
270
- disableStoreUpdates: () => void,
271
- enableStoreUpdates: () => void,
272
- |} {
273
- const fragmentSelector = getSelector(fragmentNode, fragmentRef);
294
+ ): ?SelectorData | Array<?SelectorData> {
295
+ const fragmentSelector = useMemo(
296
+ () => getSelector(fragmentNode, fragmentRef),
297
+ [fragmentNode, fragmentRef],
298
+ );
299
+
300
+ const isPlural = fragmentNode?.metadata?.plural === true;
274
301
 
275
- if (fragmentNode?.metadata?.plural === true) {
302
+ if (isPlural) {
276
303
  invariant(
277
- Array.isArray(fragmentRef),
304
+ fragmentRef == null || Array.isArray(fragmentRef),
278
305
  'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
279
306
  'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
280
307
  'from fragment `%s` to allow the prop to be an object.',
@@ -296,7 +323,9 @@ function useFragmentInternal_REACT_CACHE(
296
323
  );
297
324
  }
298
325
  invariant(
299
- fragmentRef == null || fragmentSelector != null,
326
+ fragmentRef == null ||
327
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
328
+ fragmentSelector != null,
300
329
  'Relay: Expected to receive an object where `...%s` was spread, ' +
301
330
  'but the fragment reference was not found`. This is most ' +
302
331
  'likely the result of:\n' +
@@ -317,38 +346,94 @@ function useFragmentInternal_REACT_CACHE(
317
346
 
318
347
  const environment = useRelayEnvironment();
319
348
  const [rawState, setState] = useState<FragmentState>(() =>
320
- getFragmentState(environment, fragmentSelector),
349
+ getFragmentState(environment, fragmentSelector, isPlural),
321
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);
322
371
 
323
372
  const [previousFragmentSelector, setPreviousFragmentSelector] =
324
373
  useState(fragmentSelector);
325
- if (!areEqualSelectors(fragmentSelector, previousFragmentSelector)) {
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
326
380
  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;
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;
337
392
  }
338
393
 
339
394
  // Handle the queries for any missing client edges; this may suspend.
340
395
  // 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
- }
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]);
352
437
  }
353
438
 
354
439
  if (isMissingData(state)) {
@@ -372,45 +457,103 @@ function useFragmentInternal_REACT_CACHE(
372
457
  handlePotentialSnapshotErrorsForState(environment, state);
373
458
  }
374
459
 
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
460
  useEffect(() => {
386
- const wasAlreadySubscribed = isMountedRef.current;
387
- isMountedRef.current = true;
388
- if (!wasAlreadySubscribed) {
389
- handleMissedUpdates(environment, state, setState);
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;
390
477
  }
391
- return subscribeToSnapshot(environment, state, setState);
392
- }, [environment, state]);
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]);
393
496
 
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
- );
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
+ }
403
550
 
404
551
  if (__DEV__) {
405
552
  // eslint-disable-next-line react-hooks/rules-of-hooks
406
553
  useDebugValue({fragment: fragmentNode.name, data});
407
554
  }
408
555
 
409
- return {
410
- data,
411
- disableStoreUpdates,
412
- enableStoreUpdates,
413
- };
556
+ return data;
414
557
  }
415
558
 
416
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;
@@ -26,6 +26,7 @@ const useMemoOperationDescriptor = require('../useMemoOperationDescriptor');
26
26
  const useRelayEnvironment = require('../useRelayEnvironment');
27
27
  const getQueryResultOrFetchQuery = require('./getQueryResultOrFetchQuery_REACT_CACHE');
28
28
  const useFragmentInternal = require('./useFragmentInternal_REACT_CACHE');
29
+ const {useEffect} = require('react');
29
30
 
30
31
  function useLazyLoadQuery_REACT_CACHE<TVariables: Variables, TData>(
31
32
  gqlQuery: Query<TVariables, TData>,
@@ -47,20 +48,25 @@ function useLazyLoadQuery_REACT_CACHE<TVariables: Variables, TData>(
47
48
  );
48
49
 
49
50
  // Get the query going if needed -- this may suspend.
50
- const queryResult = getQueryResultOrFetchQuery(
51
+ const [queryResult, effect] = getQueryResultOrFetchQuery(
51
52
  environment,
52
53
  queryOperationDescriptor,
53
- options?.fetchPolicy,
54
+ {
55
+ fetchPolicy: options?.fetchPolicy,
56
+ renderPolicy: options?.UNSTABLE_renderPolicy,
57
+ fetchKey: options?.fetchKey,
58
+ },
54
59
  );
55
60
 
61
+ useEffect(effect);
62
+
56
63
  // Read the query's root fragment -- this may suspend.
57
64
  const {fragmentNode, fragmentRef} = queryResult;
58
-
59
65
  // $FlowExpectedError[incompatible-return] Is this a fixable incompatible-return?
60
66
  return useFragmentInternal(fragmentNode, fragmentRef, 'useLazyLoadQuery()', {
61
67
  fetchPolicy: options?.fetchPolicy,
62
68
  networkCacheConfig: options?.networkCacheConfig,
63
- }).data;
69
+ });
64
70
  }
65
71
 
66
72
  module.exports = useLazyLoadQuery_REACT_CACHE;