react-relay 17.0.0 → 18.0.0

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