react-relay 17.0.0 → 18.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. package/ReactRelayContainerUtils.js.flow +2 -2
  2. package/ReactRelayContext.js +1 -1
  3. package/ReactRelayContext.js.flow +1 -1
  4. package/ReactRelayFragmentContainer.js.flow +2 -2
  5. package/ReactRelayPaginationContainer.js.flow +2 -2
  6. package/ReactRelayQueryRenderer.js.flow +1 -1
  7. package/ReactRelayQueryRendererContext.js.flow +1 -1
  8. package/ReactRelayRefetchContainer.js.flow +2 -2
  9. package/ReactRelayTypes.js.flow +45 -18
  10. package/__flowtests__/ReactRelayFragmentContainer-flowtest.js.flow +2 -2
  11. package/buildReactRelayContainer.js.flow +6 -5
  12. package/hooks.js +1 -1
  13. package/index.js +1 -1
  14. package/legacy.js +1 -1
  15. package/lib/relay-hooks/getConnectionState.js +47 -0
  16. package/lib/relay-hooks/legacy/FragmentResource.js +2 -6
  17. package/lib/relay-hooks/loadEntryPoint.js +8 -5
  18. package/lib/relay-hooks/loadQuery.js +2 -14
  19. package/lib/relay-hooks/readFragmentInternal.js +2 -4
  20. package/lib/relay-hooks/useEntryPointLoader.js +5 -8
  21. package/lib/relay-hooks/useFragment.js +4 -7
  22. package/lib/relay-hooks/useFragmentInternal.js +6 -484
  23. package/lib/relay-hooks/useFragmentInternal_CURRENT.js +477 -0
  24. package/lib/relay-hooks/useFragmentInternal_EXPERIMENTAL.js +499 -0
  25. package/lib/relay-hooks/useLazyLoadQuery.js +2 -5
  26. package/lib/relay-hooks/useLoadMoreFunction.js +10 -43
  27. package/lib/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js +130 -0
  28. package/lib/relay-hooks/usePreloadedQuery.js +6 -9
  29. package/lib/relay-hooks/useQueryLoader.js +9 -3
  30. package/lib/relay-hooks/useQueryLoader_EXPERIMENTAL.js +120 -0
  31. package/multi-actor/ActorChange.js.flow +1 -1
  32. package/package.json +3 -3
  33. package/react-relay-hooks.js +2 -2
  34. package/react-relay-hooks.min.js +2 -2
  35. package/react-relay-legacy.js +1 -1
  36. package/react-relay-legacy.min.js +1 -1
  37. package/react-relay.js +2 -2
  38. package/react-relay.min.js +2 -2
  39. package/relay-hooks/EntryPointTypes.flow.js.flow +35 -12
  40. package/relay-hooks/LazyLoadEntryPointContainer_DEPRECATED.react.js.flow +8 -4
  41. package/relay-hooks/MatchContainer.js.flow +1 -1
  42. package/relay-hooks/ProfilerContext.js.flow +1 -1
  43. package/relay-hooks/__flowtests__/EntryPointTypes/ExtractQueryTypes-flowtest.js.flow +43 -0
  44. package/relay-hooks/getConnectionState.js.flow +97 -0
  45. package/relay-hooks/legacy/FragmentResource.js.flow +2 -13
  46. package/relay-hooks/loadEntryPoint.js.flow +10 -4
  47. package/relay-hooks/loadQuery.js.flow +4 -28
  48. package/relay-hooks/prepareEntryPoint_DEPRECATED.js.flow +4 -1
  49. package/relay-hooks/readFragmentInternal.js.flow +1 -10
  50. package/relay-hooks/useEntryPointLoader.js.flow +3 -4
  51. package/relay-hooks/useFragment.js.flow +0 -5
  52. package/relay-hooks/useFragmentInternal.js.flow +19 -643
  53. package/relay-hooks/useFragmentInternal_CURRENT.js.flow +656 -0
  54. package/relay-hooks/useFragmentInternal_EXPERIMENTAL.js.flow +718 -0
  55. package/relay-hooks/useLazyLoadQuery.js.flow +0 -5
  56. package/relay-hooks/useLoadMoreFunction.js.flow +14 -80
  57. package/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js.flow +280 -0
  58. package/relay-hooks/usePaginationFragment.js.flow +1 -1
  59. package/relay-hooks/usePreloadedQuery.js.flow +0 -5
  60. package/relay-hooks/useQueryLoader.js.flow +28 -5
  61. package/relay-hooks/useQueryLoader_EXPERIMENTAL.js.flow +253 -0
@@ -0,0 +1,656 @@
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
+ * @format
9
+ * @oncall relay
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import type {QueryResult} from './QueryResource';
15
+ import type {
16
+ CacheConfig,
17
+ FetchPolicy,
18
+ IEnvironment,
19
+ ReaderFragment,
20
+ ReaderSelector,
21
+ SelectorData,
22
+ Snapshot,
23
+ } from 'relay-runtime';
24
+ import type {
25
+ MissingClientEdgeRequestInfo,
26
+ MissingLiveResolverField,
27
+ } from 'relay-runtime/store/RelayStoreTypes';
28
+
29
+ const {getQueryResourceForEnvironment} = require('./QueryResource');
30
+ const useRelayEnvironment = require('./useRelayEnvironment');
31
+ const invariant = require('invariant');
32
+ const {useDebugValue, useEffect, useMemo, useRef, useState} = require('react');
33
+ const {
34
+ __internal: {fetchQuery: fetchQueryInternal, getPromiseForActiveRequest},
35
+ RelayFeatureFlags,
36
+ areEqualSelectors,
37
+ createOperationDescriptor,
38
+ getPendingOperationsForFragment,
39
+ getSelector,
40
+ getVariablesFromFragment,
41
+ handlePotentialSnapshotErrors,
42
+ recycleNodesInto,
43
+ } = require('relay-runtime');
44
+ const warning = require('warning');
45
+
46
+ type FragmentQueryOptions = {
47
+ fetchPolicy?: FetchPolicy,
48
+ networkCacheConfig?: ?CacheConfig,
49
+ };
50
+
51
+ type FragmentState = $ReadOnly<
52
+ | {kind: 'bailout'}
53
+ | {kind: 'singular', snapshot: Snapshot, epoch: number}
54
+ | {kind: 'plural', snapshots: $ReadOnlyArray<Snapshot>, epoch: number},
55
+ >;
56
+
57
+ type StateUpdaterFunction<T> = ((T) => T) => void;
58
+
59
+ function isMissingData(state: FragmentState): boolean {
60
+ if (state.kind === 'bailout') {
61
+ return false;
62
+ } else if (state.kind === 'singular') {
63
+ return state.snapshot.isMissingData;
64
+ } else {
65
+ return state.snapshots.some(s => s.isMissingData);
66
+ }
67
+ }
68
+
69
+ function getMissingClientEdges(
70
+ state: FragmentState,
71
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
72
+ if (state.kind === 'bailout') {
73
+ return null;
74
+ } else if (state.kind === 'singular') {
75
+ return state.snapshot.missingClientEdges ?? null;
76
+ } else {
77
+ let edges: null | Array<MissingClientEdgeRequestInfo> = null;
78
+ for (const snapshot of state.snapshots) {
79
+ if (snapshot.missingClientEdges) {
80
+ edges = edges ?? [];
81
+ for (const edge of snapshot.missingClientEdges) {
82
+ edges.push(edge);
83
+ }
84
+ }
85
+ }
86
+ return edges;
87
+ }
88
+ }
89
+
90
+ function getSuspendingLiveResolver(
91
+ state: FragmentState,
92
+ ): $ReadOnlyArray<MissingLiveResolverField> | null {
93
+ if (state.kind === 'bailout') {
94
+ return null;
95
+ } else if (state.kind === 'singular') {
96
+ return state.snapshot.missingLiveResolverFields ?? null;
97
+ } else {
98
+ let missingFields: null | Array<MissingLiveResolverField> = null;
99
+ for (const snapshot of state.snapshots) {
100
+ if (snapshot.missingLiveResolverFields) {
101
+ missingFields = missingFields ?? [];
102
+ for (const edge of snapshot.missingLiveResolverFields) {
103
+ missingFields.push(edge);
104
+ }
105
+ }
106
+ }
107
+ return missingFields;
108
+ }
109
+ }
110
+
111
+ function handlePotentialSnapshotErrorsForState(
112
+ environment: IEnvironment,
113
+ state: FragmentState,
114
+ ): void {
115
+ if (state.kind === 'singular') {
116
+ handlePotentialSnapshotErrors(
117
+ environment,
118
+ state.snapshot.errorResponseFields,
119
+ );
120
+ } else if (state.kind === 'plural') {
121
+ for (const snapshot of state.snapshots) {
122
+ handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check for updates to the store that occurred concurrently with rendering the given `state` value,
129
+ * returning a new (updated) state if there were updates or null if there were no changes.
130
+ */
131
+ function handleMissedUpdates(
132
+ environment: IEnvironment,
133
+ state: FragmentState,
134
+ ): null | [/* has data changed */ boolean, FragmentState] {
135
+ if (state.kind === 'bailout') {
136
+ return null;
137
+ }
138
+ // FIXME this is invalid if we've just switched environments.
139
+ const currentEpoch = environment.getStore().getEpoch();
140
+ if (currentEpoch === state.epoch) {
141
+ return null;
142
+ }
143
+ // The store has updated since we rendered (without us being subscribed yet),
144
+ // so check for any updates to the data we're rendering:
145
+ if (state.kind === 'singular') {
146
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
147
+ const updatedData = recycleNodesInto(
148
+ state.snapshot.data,
149
+ currentSnapshot.data,
150
+ );
151
+ const updatedCurrentSnapshot: Snapshot = {
152
+ data: updatedData,
153
+ isMissingData: currentSnapshot.isMissingData,
154
+ missingClientEdges: currentSnapshot.missingClientEdges,
155
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
156
+ seenRecords: currentSnapshot.seenRecords,
157
+ selector: currentSnapshot.selector,
158
+ errorResponseFields: currentSnapshot.errorResponseFields,
159
+ };
160
+ return [
161
+ updatedData !== state.snapshot.data,
162
+ {
163
+ kind: 'singular',
164
+ snapshot: updatedCurrentSnapshot,
165
+ epoch: currentEpoch,
166
+ },
167
+ ];
168
+ } else {
169
+ let didMissUpdates = false;
170
+ const currentSnapshots = [];
171
+ for (let index = 0; index < state.snapshots.length; index++) {
172
+ const snapshot = state.snapshots[index];
173
+ const currentSnapshot = environment.lookup(snapshot.selector);
174
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
175
+ const updatedCurrentSnapshot: Snapshot = {
176
+ data: updatedData,
177
+ isMissingData: currentSnapshot.isMissingData,
178
+ missingClientEdges: currentSnapshot.missingClientEdges,
179
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
180
+ seenRecords: currentSnapshot.seenRecords,
181
+ selector: currentSnapshot.selector,
182
+ errorResponseFields: currentSnapshot.errorResponseFields,
183
+ };
184
+ if (updatedData !== snapshot.data) {
185
+ didMissUpdates = true;
186
+ }
187
+ currentSnapshots.push(updatedCurrentSnapshot);
188
+ }
189
+ invariant(
190
+ currentSnapshots.length === state.snapshots.length,
191
+ 'Expected same number of snapshots',
192
+ );
193
+ return [
194
+ didMissUpdates,
195
+ {
196
+ kind: 'plural',
197
+ snapshots: currentSnapshots,
198
+ epoch: currentEpoch,
199
+ },
200
+ ];
201
+ }
202
+ }
203
+
204
+ function handleMissingClientEdge(
205
+ environment: IEnvironment,
206
+ parentFragmentNode: ReaderFragment,
207
+ parentFragmentRef: mixed,
208
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
209
+ queryOptions?: FragmentQueryOptions,
210
+ ): [QueryResult, ?Promise<mixed>] {
211
+ const originalVariables = getVariablesFromFragment(
212
+ parentFragmentNode,
213
+ parentFragmentRef,
214
+ );
215
+ const variables = {
216
+ ...originalVariables,
217
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
218
+ };
219
+ const queryOperationDescriptor = createOperationDescriptor(
220
+ missingClientEdgeRequestInfo.request,
221
+ variables,
222
+ queryOptions?.networkCacheConfig,
223
+ );
224
+ // This may suspend. We don't need to do anything with the results; all we're
225
+ // doing here is started the query if needed and retaining and releasing it
226
+ // according to the component mount/suspense cycle; QueryResource
227
+ // already handles this by itself.
228
+ const QueryResource = getQueryResourceForEnvironment(environment);
229
+ const queryResult = QueryResource.prepare(
230
+ queryOperationDescriptor,
231
+ fetchQueryInternal(environment, queryOperationDescriptor),
232
+ queryOptions?.fetchPolicy,
233
+ );
234
+
235
+ return [
236
+ queryResult,
237
+ getPromiseForActiveRequest(environment, queryOperationDescriptor.request),
238
+ ];
239
+ }
240
+
241
+ function subscribeToSnapshot(
242
+ environment: IEnvironment,
243
+ state: FragmentState,
244
+ setState: StateUpdaterFunction<FragmentState>,
245
+ hasPendingStateChanges: {current: boolean},
246
+ ): () => void {
247
+ if (state.kind === 'bailout') {
248
+ return () => {};
249
+ } else if (state.kind === 'singular') {
250
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
251
+ setState(prevState => {
252
+ // In theory a setState from a subscription could be batched together
253
+ // with a setState to change the fragment selector. Guard against this
254
+ // by bailing out of the state update if the selector has changed.
255
+ if (
256
+ prevState.kind !== 'singular' ||
257
+ prevState.snapshot.selector !== latestSnapshot.selector
258
+ ) {
259
+ const updates = handleMissedUpdates(environment, prevState);
260
+ if (updates != null) {
261
+ const [dataChanged, nextState] = updates;
262
+ environment.__log({
263
+ name: 'useFragment.subscription.missedUpdates',
264
+ hasDataChanges: dataChanged,
265
+ });
266
+ hasPendingStateChanges.current = dataChanged;
267
+ return dataChanged ? nextState : prevState;
268
+ } else {
269
+ return prevState;
270
+ }
271
+ }
272
+
273
+ hasPendingStateChanges.current = true;
274
+ return {
275
+ kind: 'singular',
276
+ snapshot: latestSnapshot,
277
+ epoch: environment.getStore().getEpoch(),
278
+ };
279
+ });
280
+ });
281
+ return () => {
282
+ disposable.dispose();
283
+ };
284
+ } else {
285
+ const disposables = state.snapshots.map((snapshot, index) =>
286
+ environment.subscribe(snapshot, latestSnapshot => {
287
+ setState(prevState => {
288
+ // In theory a setState from a subscription could be batched together
289
+ // with a setState to change the fragment selector. Guard against this
290
+ // by bailing out of the state update if the selector has changed.
291
+ if (
292
+ prevState.kind !== 'plural' ||
293
+ prevState.snapshots[index]?.selector !== latestSnapshot.selector
294
+ ) {
295
+ const updates = handleMissedUpdates(environment, prevState);
296
+ if (updates != null) {
297
+ const [dataChanged, nextState] = updates;
298
+ environment.__log({
299
+ name: 'useFragment.subscription.missedUpdates',
300
+ hasDataChanges: dataChanged,
301
+ });
302
+ hasPendingStateChanges.current =
303
+ hasPendingStateChanges.current || dataChanged;
304
+ return dataChanged ? nextState : prevState;
305
+ } else {
306
+ return prevState;
307
+ }
308
+ }
309
+ const updated = [...prevState.snapshots];
310
+ updated[index] = latestSnapshot;
311
+ hasPendingStateChanges.current = true;
312
+ return {
313
+ kind: 'plural',
314
+ snapshots: updated,
315
+ epoch: environment.getStore().getEpoch(),
316
+ };
317
+ });
318
+ }),
319
+ );
320
+ return () => {
321
+ for (const d of disposables) {
322
+ d.dispose();
323
+ }
324
+ };
325
+ }
326
+ }
327
+
328
+ function getFragmentState(
329
+ environment: IEnvironment,
330
+ fragmentSelector: ?ReaderSelector,
331
+ ): FragmentState {
332
+ if (fragmentSelector == null) {
333
+ return {kind: 'bailout'};
334
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
335
+ // Note that if fragmentRef is an empty array, fragmentSelector will be null so we'll hit the above case.
336
+ // Null is returned by getSelector if fragmentRef has no non-null items.
337
+ return {
338
+ kind: 'plural',
339
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
340
+ epoch: environment.getStore().getEpoch(),
341
+ };
342
+ } else {
343
+ return {
344
+ kind: 'singular',
345
+ snapshot: environment.lookup(fragmentSelector),
346
+ epoch: environment.getStore().getEpoch(),
347
+ };
348
+ }
349
+ }
350
+
351
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
352
+ hook useFragmentInternal(
353
+ fragmentNode: ReaderFragment,
354
+ fragmentRef: mixed,
355
+ hookDisplayName: string,
356
+ queryOptions?: FragmentQueryOptions,
357
+ ): ?SelectorData | Array<?SelectorData> {
358
+ const fragmentSelector = useMemo(
359
+ () => getSelector(fragmentNode, fragmentRef),
360
+ [fragmentNode, fragmentRef],
361
+ );
362
+
363
+ const isPlural = fragmentNode?.metadata?.plural === true;
364
+
365
+ if (isPlural) {
366
+ invariant(
367
+ fragmentRef == null || Array.isArray(fragmentRef),
368
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
369
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
370
+ 'from fragment `%s` to allow the prop to be an object.',
371
+ fragmentNode.name,
372
+ typeof fragmentRef,
373
+ fragmentNode.name,
374
+ );
375
+ } else {
376
+ invariant(
377
+ !Array.isArray(fragmentRef),
378
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
379
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
380
+ 'to fragment `%s` to allow the prop to be an array.',
381
+ fragmentNode.name,
382
+ typeof fragmentRef,
383
+ fragmentNode.name,
384
+ );
385
+ }
386
+ invariant(
387
+ fragmentRef == null ||
388
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
389
+ fragmentSelector != null,
390
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
391
+ 'but the fragment reference was not found`. This is most ' +
392
+ 'likely the result of:\n' +
393
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
394
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
395
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
396
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
397
+ 'spread - then the fragment reference will not exist. ' +
398
+ 'In this case, pass `null` if the conditions for evaluating the ' +
399
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
400
+ fragmentNode.name,
401
+ fragmentNode.name,
402
+ hookDisplayName,
403
+ fragmentNode.name,
404
+ hookDisplayName,
405
+ );
406
+
407
+ const environment = useRelayEnvironment();
408
+ const [_state, setState] = useState<FragmentState>(() =>
409
+ getFragmentState(environment, fragmentSelector),
410
+ );
411
+ let state = _state;
412
+
413
+ // This copy of the state we only update when something requires us to
414
+ // unsubscribe and re-subscribe, namely a changed environment or
415
+ // fragment selector.
416
+ const [_subscribedState, setSubscribedState] = useState(state);
417
+ // FIXME since this is used as an effect dependency, it needs to be memoized.
418
+ let subscribedState = _subscribedState;
419
+
420
+ const [previousFragmentSelector, setPreviousFragmentSelector] =
421
+ useState(fragmentSelector);
422
+ const [previousEnvironment, setPreviousEnvironment] = useState(environment);
423
+ if (
424
+ !areEqualSelectors(fragmentSelector, previousFragmentSelector) ||
425
+ environment !== previousEnvironment
426
+ ) {
427
+ // Enqueue setState to record the new selector and state
428
+ setPreviousFragmentSelector(fragmentSelector);
429
+ setPreviousEnvironment(environment);
430
+ const newState = getFragmentState(environment, fragmentSelector);
431
+ setState(newState);
432
+ setSubscribedState(newState); // This causes us to form a new subscription
433
+ // But render with the latest state w/o waiting for the setState. Otherwise
434
+ // the component would render the wrong information temporarily (including
435
+ // possibly incorrectly triggering some warnings below).
436
+ state = newState;
437
+ subscribedState = newState;
438
+ }
439
+
440
+ // The purpose of this is to detect whether we have ever committed, because we
441
+ // don't suspend on store updates, only when the component either is first trying
442
+ // to mount or when the our selector changes. The selector change in particular is
443
+ // how we suspend for pagination and refetch. Also, fragment selector can be null
444
+ // or undefined, so we use false as a special value to distinguish from all fragment
445
+ // selectors; false means that the component hasn't mounted yet.
446
+ const committedFragmentSelectorRef = useRef<false | ?ReaderSelector>(false);
447
+ useEffect(() => {
448
+ committedFragmentSelectorRef.current = fragmentSelector;
449
+ }, [fragmentSelector]);
450
+
451
+ // Handle the queries for any missing client edges; this may suspend.
452
+ // FIXME handle client edges in parallel.
453
+ if (fragmentNode.metadata?.hasClientEdges === true) {
454
+ // The fragment is validated to be static (in useFragment) and hasClientEdges is
455
+ // a static (constant) property of the fragment. In practice, this effect will
456
+ // always or never run for a given invocation of this hook.
457
+ // eslint-disable-next-line react-hooks/rules-of-hooks
458
+ // $FlowFixMe[react-rule-hook]
459
+ const [clientEdgeQueries, activeRequestPromises] = useMemo(() => {
460
+ const missingClientEdges = getMissingClientEdges(state);
461
+ // eslint-disable-next-line no-shadow
462
+ let clientEdgeQueries;
463
+ const activeRequestPromises = [];
464
+ if (missingClientEdges?.length) {
465
+ clientEdgeQueries = ([]: Array<QueryResult>);
466
+ for (const edge of missingClientEdges) {
467
+ const [queryResult, requestPromise] = handleMissingClientEdge(
468
+ environment,
469
+ fragmentNode,
470
+ fragmentRef,
471
+ edge,
472
+ queryOptions,
473
+ );
474
+ clientEdgeQueries.push(queryResult);
475
+ if (requestPromise != null) {
476
+ activeRequestPromises.push(requestPromise);
477
+ }
478
+ }
479
+ }
480
+ return [clientEdgeQueries, activeRequestPromises];
481
+ }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
482
+
483
+ if (activeRequestPromises.length) {
484
+ throw Promise.all(activeRequestPromises);
485
+ }
486
+
487
+ // See above note
488
+ // eslint-disable-next-line react-hooks/rules-of-hooks
489
+ // $FlowFixMe[react-rule-hook]
490
+ useEffect(() => {
491
+ const QueryResource = getQueryResourceForEnvironment(environment);
492
+ if (clientEdgeQueries?.length) {
493
+ const disposables = [];
494
+ for (const query of clientEdgeQueries) {
495
+ disposables.push(QueryResource.retain(query));
496
+ }
497
+ return () => {
498
+ for (const disposable of disposables) {
499
+ disposable.dispose();
500
+ }
501
+ };
502
+ }
503
+ }, [environment, clientEdgeQueries]);
504
+ }
505
+
506
+ if (isMissingData(state)) {
507
+ // Suspend if a Live Resolver within this fragment is in a suspended state:
508
+ const suspendingLiveResolvers = getSuspendingLiveResolver(state);
509
+ if (suspendingLiveResolvers != null && suspendingLiveResolvers.length > 0) {
510
+ throw Promise.all(
511
+ suspendingLiveResolvers.map(({liveStateID}) => {
512
+ // $FlowFixMe[prop-missing] This is expected to be a RelayModernStore
513
+ return environment.getStore().getLiveResolverPromise(liveStateID);
514
+ }),
515
+ );
516
+ }
517
+ // Suspend if an active operation bears on this fragment, either the
518
+ // fragment's owner or some other mutation etc. that could affect it.
519
+ // We only suspend when the component is first trying to mount or changing
520
+ // selectors, not if data becomes missing later:
521
+ if (
522
+ RelayFeatureFlags.ENABLE_RELAY_OPERATION_TRACKER_SUSPENSE ||
523
+ environment !== previousEnvironment ||
524
+ !committedFragmentSelectorRef.current ||
525
+ // $FlowFixMe[react-rule-unsafe-ref]
526
+ !areEqualSelectors(committedFragmentSelectorRef.current, fragmentSelector)
527
+ ) {
528
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
529
+ const fragmentOwner =
530
+ fragmentSelector.kind === 'PluralReaderSelector'
531
+ ? fragmentSelector.selectors[0].owner
532
+ : fragmentSelector.owner;
533
+ const pendingOperationsResult = getPendingOperationsForFragment(
534
+ environment,
535
+ fragmentNode,
536
+ fragmentOwner,
537
+ );
538
+ if (pendingOperationsResult) {
539
+ throw pendingOperationsResult.promise;
540
+ }
541
+ }
542
+ }
543
+
544
+ // Report required fields only if we're not suspending, since that means
545
+ // they're missing even though we are out of options for possibly fetching them:
546
+ handlePotentialSnapshotErrorsForState(environment, state);
547
+
548
+ const hasPendingStateChanges = useRef<boolean>(false);
549
+
550
+ useEffect(() => {
551
+ // Check for updates since the state was rendered
552
+ let currentState = subscribedState;
553
+ const updates = handleMissedUpdates(environment, subscribedState);
554
+ if (updates !== null) {
555
+ const [didMissUpdates, updatedState] = updates;
556
+ // TODO: didMissUpdates only checks for changes to snapshot data, but it's possible
557
+ // that other snapshot properties may have changed that should also trigger a re-render,
558
+ // such as changed missing resolver fields, missing client edges, etc.
559
+ // A potential alternative is for handleMissedUpdates() to recycle the entire state
560
+ // value, and return the new (recycled) state only if there was some change. In that
561
+ // case the code would always setState if something in the snapshot changed, in addition
562
+ // to using the latest snapshot to subscribe.
563
+ if (didMissUpdates) {
564
+ setState(updatedState);
565
+ }
566
+ currentState = updatedState;
567
+ }
568
+ return subscribeToSnapshot(
569
+ environment,
570
+ currentState,
571
+ setState,
572
+ hasPendingStateChanges,
573
+ );
574
+ }, [environment, subscribedState]);
575
+
576
+ if (hasPendingStateChanges.current) {
577
+ const updates = handleMissedUpdates(environment, state);
578
+ if (updates != null) {
579
+ const [hasStateUpdates, updatedState] = updates;
580
+ if (hasStateUpdates) {
581
+ setState(updatedState);
582
+ state = updatedState;
583
+ }
584
+ }
585
+ // $FlowFixMe[react-rule-unsafe-ref]
586
+ hasPendingStateChanges.current = false;
587
+ }
588
+
589
+ let data: ?SelectorData | Array<?SelectorData>;
590
+ if (isPlural) {
591
+ // Plural fragments require allocating an array of the snapshot data values,
592
+ // which has to be memoized to avoid triggering downstream re-renders.
593
+ //
594
+ // Note that isPlural is a constant property of the fragment and does not change
595
+ // for a particular useFragment invocation site
596
+ const fragmentRefIsNullish = fragmentRef == null; // for less sensitive memoization
597
+ // eslint-disable-next-line react-hooks/rules-of-hooks
598
+ // $FlowFixMe[react-rule-hook]
599
+ data = useMemo(() => {
600
+ if (state.kind === 'bailout') {
601
+ // Bailout state can happen if the fragmentRef is a plural array that is empty or has no
602
+ // non-null entries. In that case, the compatible behavior is to return [] instead of null.
603
+ return fragmentRefIsNullish ? null : [];
604
+ } else {
605
+ invariant(
606
+ state.kind === 'plural',
607
+ 'Expected state to be plural because fragment is plural',
608
+ );
609
+ return state.snapshots.map(s => s.data);
610
+ }
611
+ }, [state, fragmentRefIsNullish]);
612
+ } else if (state.kind === 'bailout') {
613
+ // This case doesn't allocate a new object so it doesn't have to be memoized
614
+ data = null;
615
+ } else {
616
+ // This case doesn't allocate a new object so it doesn't have to be memoized
617
+ invariant(
618
+ state.kind === 'singular',
619
+ 'Expected state to be singular because fragment is singular',
620
+ );
621
+ data = state.snapshot.data;
622
+ }
623
+
624
+ if (RelayFeatureFlags.LOG_MISSING_RECORDS_IN_PROD || __DEV__) {
625
+ if (
626
+ fragmentRef != null &&
627
+ (data === undefined ||
628
+ (Array.isArray(data) &&
629
+ data.length > 0 &&
630
+ data.every(d => d === undefined)))
631
+ ) {
632
+ warning(
633
+ false,
634
+ 'Relay: Expected to have been able to read non-null data for ' +
635
+ 'fragment `%s` declared in ' +
636
+ '`%s`, since fragment reference was non-null. ' +
637
+ "Make sure that that `%s`'s parent isn't " +
638
+ 'holding on to and/or passing a fragment reference for data that ' +
639
+ 'has been deleted.',
640
+ fragmentNode.name,
641
+ hookDisplayName,
642
+ hookDisplayName,
643
+ );
644
+ }
645
+ }
646
+
647
+ if (__DEV__) {
648
+ // eslint-disable-next-line react-hooks/rules-of-hooks
649
+ // $FlowFixMe[react-rule-hook]
650
+ useDebugValue({fragment: fragmentNode.name, data});
651
+ }
652
+
653
+ return data;
654
+ }
655
+
656
+ module.exports = useFragmentInternal;