react-relay 13.2.0 → 14.0.0

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