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,764 @@
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
+ export type FragmentQueryOptions = {
47
+ fetchPolicy?: FetchPolicy,
48
+ networkCacheConfig?: ?CacheConfig,
49
+ };
50
+
51
+ type FragmentState = $ReadOnly<
52
+ | {kind: 'bailout', environment: IEnvironment}
53
+ | {
54
+ kind: 'singular',
55
+ snapshot: Snapshot,
56
+ epoch: number,
57
+ selector: ReaderSelector,
58
+ environment: IEnvironment,
59
+ }
60
+ | {
61
+ kind: 'plural',
62
+ snapshots: $ReadOnlyArray<Snapshot>,
63
+ epoch: number,
64
+ selector: ReaderSelector,
65
+ environment: IEnvironment,
66
+ },
67
+ >;
68
+
69
+ type StateUpdaterFunction<T> = ((T) => T) => void;
70
+
71
+ function isMissingData(state: FragmentState): boolean {
72
+ if (state.kind === 'bailout') {
73
+ return false;
74
+ } else if (state.kind === 'singular') {
75
+ return state.snapshot.isMissingData;
76
+ } else {
77
+ return state.snapshots.some(s => s.isMissingData);
78
+ }
79
+ }
80
+
81
+ function getMissingClientEdges(
82
+ state: FragmentState,
83
+ ): $ReadOnlyArray<MissingClientEdgeRequestInfo> | null {
84
+ if (state.kind === 'bailout') {
85
+ return null;
86
+ } else if (state.kind === 'singular') {
87
+ return state.snapshot.missingClientEdges ?? null;
88
+ } else {
89
+ let edges: null | Array<MissingClientEdgeRequestInfo> = null;
90
+ for (const snapshot of state.snapshots) {
91
+ if (snapshot.missingClientEdges) {
92
+ edges = edges ?? [];
93
+ for (const edge of snapshot.missingClientEdges) {
94
+ edges.push(edge);
95
+ }
96
+ }
97
+ }
98
+ return edges;
99
+ }
100
+ }
101
+
102
+ function getSuspendingLiveResolver(
103
+ state: FragmentState,
104
+ ): $ReadOnlyArray<MissingLiveResolverField> | null {
105
+ if (state.kind === 'bailout') {
106
+ return null;
107
+ } else if (state.kind === 'singular') {
108
+ return state.snapshot.missingLiveResolverFields ?? null;
109
+ } else {
110
+ let missingFields: null | Array<MissingLiveResolverField> = null;
111
+ for (const snapshot of state.snapshots) {
112
+ if (snapshot.missingLiveResolverFields) {
113
+ missingFields = missingFields ?? [];
114
+ for (const edge of snapshot.missingLiveResolverFields) {
115
+ missingFields.push(edge);
116
+ }
117
+ }
118
+ }
119
+ return missingFields;
120
+ }
121
+ }
122
+
123
+ function handlePotentialSnapshotErrorsForState(
124
+ environment: IEnvironment,
125
+ state: FragmentState,
126
+ ): void {
127
+ if (state.kind === 'singular') {
128
+ handlePotentialSnapshotErrors(
129
+ environment,
130
+ state.snapshot.missingRequiredFields,
131
+ state.snapshot.relayResolverErrors,
132
+ state.snapshot.errorResponseFields,
133
+ state.snapshot.selector.node.metadata?.throwOnFieldError ?? false,
134
+ );
135
+ } else if (state.kind === 'plural') {
136
+ for (const snapshot of state.snapshots) {
137
+ handlePotentialSnapshotErrors(
138
+ environment,
139
+ snapshot.missingRequiredFields,
140
+ snapshot.relayResolverErrors,
141
+ snapshot.errorResponseFields,
142
+ snapshot.selector.node.metadata?.throwOnFieldError ?? false,
143
+ );
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Check for updates to the store that occurred concurrently with rendering the given `state` value,
150
+ * returning a new (updated) state if there were updates or null if there were no changes.
151
+ */
152
+ function handleMissedUpdates(
153
+ environment: IEnvironment,
154
+ state: FragmentState,
155
+ ): null | [/* has data changed */ boolean, FragmentState] {
156
+ if (state.kind === 'bailout') {
157
+ return null;
158
+ }
159
+ // FIXME this is invalid if we've just switched environments.
160
+ const currentEpoch = environment.getStore().getEpoch();
161
+ if (currentEpoch === state.epoch) {
162
+ return null;
163
+ }
164
+ // The store has updated since we rendered (without us being subscribed yet),
165
+ // so check for any updates to the data we're rendering:
166
+ if (state.kind === 'singular') {
167
+ const currentSnapshot = environment.lookup(state.snapshot.selector);
168
+ const updatedData = recycleNodesInto(
169
+ state.snapshot.data,
170
+ currentSnapshot.data,
171
+ );
172
+ const updatedCurrentSnapshot: Snapshot = {
173
+ data: updatedData,
174
+ isMissingData: currentSnapshot.isMissingData,
175
+ missingClientEdges: currentSnapshot.missingClientEdges,
176
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
177
+ seenRecords: currentSnapshot.seenRecords,
178
+ selector: currentSnapshot.selector,
179
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
180
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
181
+ errorResponseFields: currentSnapshot.errorResponseFields,
182
+ };
183
+ return [
184
+ updatedData !== state.snapshot.data,
185
+ {
186
+ kind: 'singular',
187
+ snapshot: updatedCurrentSnapshot,
188
+ epoch: currentEpoch,
189
+ selector: state.selector,
190
+ environment: state.environment,
191
+ },
192
+ ];
193
+ } else {
194
+ let didMissUpdates = false;
195
+ const currentSnapshots = [];
196
+ for (let index = 0; index < state.snapshots.length; index++) {
197
+ const snapshot = state.snapshots[index];
198
+ const currentSnapshot = environment.lookup(snapshot.selector);
199
+ const updatedData = recycleNodesInto(snapshot.data, currentSnapshot.data);
200
+ const updatedCurrentSnapshot: Snapshot = {
201
+ data: updatedData,
202
+ isMissingData: currentSnapshot.isMissingData,
203
+ missingClientEdges: currentSnapshot.missingClientEdges,
204
+ missingLiveResolverFields: currentSnapshot.missingLiveResolverFields,
205
+ seenRecords: currentSnapshot.seenRecords,
206
+ selector: currentSnapshot.selector,
207
+ missingRequiredFields: currentSnapshot.missingRequiredFields,
208
+ relayResolverErrors: currentSnapshot.relayResolverErrors,
209
+ errorResponseFields: currentSnapshot.errorResponseFields,
210
+ };
211
+ if (updatedData !== snapshot.data) {
212
+ didMissUpdates = true;
213
+ }
214
+ currentSnapshots.push(updatedCurrentSnapshot);
215
+ }
216
+ invariant(
217
+ currentSnapshots.length === state.snapshots.length,
218
+ 'Expected same number of snapshots',
219
+ );
220
+ return [
221
+ didMissUpdates,
222
+ {
223
+ kind: 'plural',
224
+ snapshots: currentSnapshots,
225
+ epoch: currentEpoch,
226
+ selector: state.selector,
227
+ environment: state.environment,
228
+ },
229
+ ];
230
+ }
231
+ }
232
+
233
+ function handleMissingClientEdge(
234
+ environment: IEnvironment,
235
+ parentFragmentNode: ReaderFragment,
236
+ parentFragmentRef: mixed,
237
+ missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
238
+ queryOptions?: FragmentQueryOptions,
239
+ ): [QueryResult, ?Promise<mixed>] {
240
+ const originalVariables = getVariablesFromFragment(
241
+ parentFragmentNode,
242
+ parentFragmentRef,
243
+ );
244
+ const variables = {
245
+ ...originalVariables,
246
+ id: missingClientEdgeRequestInfo.clientEdgeDestinationID, // TODO should be a reserved name
247
+ };
248
+ const queryOperationDescriptor = createOperationDescriptor(
249
+ missingClientEdgeRequestInfo.request,
250
+ variables,
251
+ queryOptions?.networkCacheConfig,
252
+ );
253
+ // This may suspend. We don't need to do anything with the results; all we're
254
+ // doing here is started the query if needed and retaining and releasing it
255
+ // according to the component mount/suspense cycle; QueryResource
256
+ // already handles this by itself.
257
+ const QueryResource = getQueryResourceForEnvironment(environment);
258
+ const queryResult = QueryResource.prepare(
259
+ queryOperationDescriptor,
260
+ fetchQueryInternal(environment, queryOperationDescriptor),
261
+ queryOptions?.fetchPolicy,
262
+ );
263
+
264
+ return [
265
+ queryResult,
266
+ getPromiseForActiveRequest(environment, queryOperationDescriptor.request),
267
+ ];
268
+ }
269
+
270
+ function subscribeToSnapshot(
271
+ environment: IEnvironment,
272
+ state: FragmentState,
273
+ setState: StateUpdaterFunction<FragmentState>,
274
+ pendingStateRef: {current: number | null},
275
+ ): () => void {
276
+ if (state.kind === 'bailout') {
277
+ return () => {};
278
+ } else if (state.kind === 'singular') {
279
+ const disposable = environment.subscribe(state.snapshot, latestSnapshot => {
280
+ setState(prevState => {
281
+ // In theory a setState from a subscription could be batched together
282
+ // with a setState to change the fragment selector. Guard against this
283
+ // by bailing out of the state update if the selector has changed.
284
+ let nextState: FragmentState | null = null;
285
+ if (
286
+ prevState.kind !== 'singular' ||
287
+ prevState.snapshot.selector !== latestSnapshot.selector ||
288
+ prevState.environment !== environment
289
+ ) {
290
+ const updates = handleMissedUpdates(prevState.environment, prevState);
291
+ if (updates != null) {
292
+ const [dataChanged, updatedState] = updates;
293
+ environment.__log({
294
+ name: 'useFragment.subscription.missedUpdates',
295
+ hasDataChanges: dataChanged,
296
+ });
297
+ nextState = dataChanged ? updatedState : prevState;
298
+ } else {
299
+ nextState = prevState;
300
+ }
301
+ } else {
302
+ nextState = {
303
+ kind: 'singular',
304
+ snapshot: latestSnapshot,
305
+ epoch: environment.getStore().getEpoch(),
306
+ selector: state.selector,
307
+ environment: state.environment,
308
+ };
309
+ }
310
+ pendingStateRef.current =
311
+ nextState.kind === 'singular' ? nextState.epoch : null;
312
+ return nextState;
313
+ });
314
+ });
315
+ return () => {
316
+ disposable.dispose();
317
+ };
318
+ } else {
319
+ const disposables = state.snapshots.map((snapshot, index) =>
320
+ environment.subscribe(snapshot, latestSnapshot => {
321
+ setState(prevState => {
322
+ // In theory a setState from a subscription could be batched together
323
+ // with a setState to change the fragment selector. Guard against this
324
+ // by bailing out of the state update if the selector has changed.
325
+ let nextState: FragmentState | null = null;
326
+ if (
327
+ prevState.kind !== 'plural' ||
328
+ prevState.snapshots[index]?.selector !== latestSnapshot.selector ||
329
+ prevState.environment !== environment
330
+ ) {
331
+ const updates = handleMissedUpdates(
332
+ prevState.environment,
333
+ prevState,
334
+ );
335
+ if (updates != null) {
336
+ const [dataChanged, updatedState] = updates;
337
+ environment.__log({
338
+ name: 'useFragment.subscription.missedUpdates',
339
+ hasDataChanges: dataChanged,
340
+ });
341
+ nextState = dataChanged ? updatedState : prevState;
342
+ } else {
343
+ nextState = prevState;
344
+ }
345
+ } else {
346
+ const updated = [...prevState.snapshots];
347
+ updated[index] = latestSnapshot;
348
+ nextState = {
349
+ kind: 'plural',
350
+ snapshots: updated,
351
+ epoch: environment.getStore().getEpoch(),
352
+ selector: state.selector,
353
+ environment: state.environment,
354
+ };
355
+ }
356
+ pendingStateRef.current =
357
+ nextState.kind === 'plural' ? nextState.epoch : null;
358
+ return nextState;
359
+ });
360
+ }),
361
+ );
362
+ return () => {
363
+ for (const d of disposables) {
364
+ d.dispose();
365
+ }
366
+ };
367
+ }
368
+ }
369
+
370
+ function getFragmentState(
371
+ environment: IEnvironment,
372
+ fragmentSelector: ?ReaderSelector,
373
+ ): FragmentState {
374
+ if (fragmentSelector == null) {
375
+ return {kind: 'bailout', environment};
376
+ } else if (fragmentSelector.kind === 'PluralReaderSelector') {
377
+ // Note that if fragmentRef is an empty array, fragmentSelector will be null so we'll hit the above case.
378
+ // Null is returned by getSelector if fragmentRef has no non-null items.
379
+ return {
380
+ kind: 'plural',
381
+ snapshots: fragmentSelector.selectors.map(s => environment.lookup(s)),
382
+ epoch: environment.getStore().getEpoch(),
383
+ selector: fragmentSelector,
384
+ environment: environment,
385
+ };
386
+ } else {
387
+ return {
388
+ kind: 'singular',
389
+ snapshot: environment.lookup(fragmentSelector),
390
+ epoch: environment.getStore().getEpoch(),
391
+ selector: fragmentSelector,
392
+ environment: environment,
393
+ };
394
+ }
395
+ }
396
+
397
+ // fragmentNode cannot change during the lifetime of the component, though fragmentRef may change.
398
+ hook useFragmentInternal_EXPERIMENTAL(
399
+ fragmentNode: ReaderFragment,
400
+ fragmentRef: mixed,
401
+ hookDisplayName: string,
402
+ queryOptions?: FragmentQueryOptions,
403
+ ): ?SelectorData | Array<?SelectorData> {
404
+ const fragmentSelector = useMemo(
405
+ () => getSelector(fragmentNode, fragmentRef),
406
+ [fragmentNode, fragmentRef],
407
+ );
408
+
409
+ const isPlural = fragmentNode?.metadata?.plural === true;
410
+
411
+ if (isPlural) {
412
+ invariant(
413
+ fragmentRef == null || Array.isArray(fragmentRef),
414
+ 'Relay: Expected fragment pointer%s for fragment `%s` to be ' +
415
+ 'an array, instead got `%s`. Remove `@relay(plural: true)` ' +
416
+ 'from fragment `%s` to allow the prop to be an object.',
417
+ fragmentNode.name,
418
+ typeof fragmentRef,
419
+ fragmentNode.name,
420
+ );
421
+ } else {
422
+ invariant(
423
+ !Array.isArray(fragmentRef),
424
+ 'Relay: Expected fragment pointer%s for fragment `%s` not to be ' +
425
+ 'an array, instead got `%s`. Add `@relay(plural: true)` ' +
426
+ 'to fragment `%s` to allow the prop to be an array.',
427
+ fragmentNode.name,
428
+ typeof fragmentRef,
429
+ fragmentNode.name,
430
+ );
431
+ }
432
+ invariant(
433
+ fragmentRef == null ||
434
+ (isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0) ||
435
+ fragmentSelector != null,
436
+ 'Relay: Expected to receive an object where `...%s` was spread, ' +
437
+ 'but the fragment reference was not found`. This is most ' +
438
+ 'likely the result of:\n' +
439
+ "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" +
440
+ '- Conditionally fetching `%s` but unconditionally passing %s prop ' +
441
+ 'to `%s`. If the parent fragment only fetches the fragment conditionally ' +
442
+ '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' +
443
+ 'spread - then the fragment reference will not exist. ' +
444
+ 'In this case, pass `null` if the conditions for evaluating the ' +
445
+ 'fragment are not met (e.g. if the `@include(if)` value is false.)',
446
+ fragmentNode.name,
447
+ fragmentNode.name,
448
+ hookDisplayName,
449
+ fragmentNode.name,
450
+ hookDisplayName,
451
+ );
452
+
453
+ const environment = useRelayEnvironment();
454
+ const [_state, setState] = useState<FragmentState>(() =>
455
+ getFragmentState(environment, fragmentSelector),
456
+ );
457
+ let state = _state;
458
+ const previousEnvironment = state.environment;
459
+
460
+ if (
461
+ !areEqualSelectors(fragmentSelector, state.selector) ||
462
+ environment !== state.environment
463
+ ) {
464
+ // Enqueue setState to record the new selector and state
465
+ const newState = getFragmentState(environment, fragmentSelector);
466
+ setState(newState);
467
+ // But render with the latest state w/o waiting for the setState. Otherwise
468
+ // the component would render the wrong information temporarily (including
469
+ // possibly incorrectly triggering some warnings below).
470
+ state = newState;
471
+ }
472
+
473
+ // The purpose of this is to detect whether we have ever committed, because we
474
+ // don't suspend on store updates, only when the component either is first trying
475
+ // to mount or when the our selector changes. The selector change in particular is
476
+ // how we suspend for pagination and refetch. Also, fragment selector can be null
477
+ // or undefined, so we use false as a special value to distinguish from all fragment
478
+ // selectors; false means that the component hasn't mounted yet.
479
+ const committedFragmentSelectorRef = useRef<false | ?ReaderSelector>(false);
480
+ useEffect(() => {
481
+ committedFragmentSelectorRef.current = fragmentSelector;
482
+ }, [fragmentSelector]);
483
+
484
+ // Handle the queries for any missing client edges; this may suspend.
485
+ // FIXME handle client edges in parallel.
486
+ if (fragmentNode.metadata?.hasClientEdges === true) {
487
+ // The fragment is validated to be static (in useFragment) and hasClientEdges is
488
+ // a static (constant) property of the fragment. In practice, this effect will
489
+ // always or never run for a given invocation of this hook.
490
+ // eslint-disable-next-line react-hooks/rules-of-hooks
491
+ // $FlowFixMe[react-rule-hook]
492
+ const [clientEdgeQueries, activeRequestPromises] = useMemo(() => {
493
+ const missingClientEdges = getMissingClientEdges(state);
494
+ // eslint-disable-next-line no-shadow
495
+ let clientEdgeQueries;
496
+ const activeRequestPromises = [];
497
+ if (missingClientEdges?.length) {
498
+ clientEdgeQueries = ([]: Array<QueryResult>);
499
+ for (const edge of missingClientEdges) {
500
+ const [queryResult, requestPromise] = handleMissingClientEdge(
501
+ environment,
502
+ fragmentNode,
503
+ fragmentRef,
504
+ edge,
505
+ queryOptions,
506
+ );
507
+ clientEdgeQueries.push(queryResult);
508
+ if (requestPromise != null) {
509
+ activeRequestPromises.push(requestPromise);
510
+ }
511
+ }
512
+ }
513
+ return [clientEdgeQueries, activeRequestPromises];
514
+ }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
515
+
516
+ if (activeRequestPromises.length) {
517
+ throw Promise.all(activeRequestPromises);
518
+ }
519
+
520
+ // See above note
521
+ // eslint-disable-next-line react-hooks/rules-of-hooks
522
+ // $FlowFixMe[react-rule-hook]
523
+ useEffect(() => {
524
+ const QueryResource = getQueryResourceForEnvironment(environment);
525
+ if (clientEdgeQueries?.length) {
526
+ const disposables = [];
527
+ for (const query of clientEdgeQueries) {
528
+ disposables.push(QueryResource.retain(query));
529
+ }
530
+ return () => {
531
+ for (const disposable of disposables) {
532
+ disposable.dispose();
533
+ }
534
+ };
535
+ }
536
+ }, [environment, clientEdgeQueries]);
537
+ }
538
+
539
+ if (isMissingData(state)) {
540
+ // Suspend if a Live Resolver within this fragment is in a suspended state:
541
+ const suspendingLiveResolvers = getSuspendingLiveResolver(state);
542
+ if (suspendingLiveResolvers != null && suspendingLiveResolvers.length > 0) {
543
+ throw Promise.all(
544
+ suspendingLiveResolvers.map(({liveStateID}) => {
545
+ // $FlowFixMe[prop-missing] This is expected to be a LiveResolverStore
546
+ return environment.getStore().getLiveResolverPromise(liveStateID);
547
+ }),
548
+ );
549
+ }
550
+ // Suspend if an active operation bears on this fragment, either the
551
+ // fragment's owner or some other mutation etc. that could affect it.
552
+ // We only suspend when the component is first trying to mount or changing
553
+ // selectors, not if data becomes missing later:
554
+ if (
555
+ RelayFeatureFlags.ENABLE_RELAY_OPERATION_TRACKER_SUSPENSE ||
556
+ environment !== previousEnvironment ||
557
+ !committedFragmentSelectorRef.current ||
558
+ // $FlowFixMe[react-rule-unsafe-ref]
559
+ !areEqualSelectors(committedFragmentSelectorRef.current, fragmentSelector)
560
+ ) {
561
+ invariant(fragmentSelector != null, 'refinement, see invariants above');
562
+ const fragmentOwner =
563
+ fragmentSelector.kind === 'PluralReaderSelector'
564
+ ? fragmentSelector.selectors[0].owner
565
+ : fragmentSelector.owner;
566
+ const pendingOperationsResult = getPendingOperationsForFragment(
567
+ environment,
568
+ fragmentNode,
569
+ fragmentOwner,
570
+ );
571
+ if (pendingOperationsResult) {
572
+ throw pendingOperationsResult.promise;
573
+ }
574
+ }
575
+ }
576
+
577
+ // Report required fields only if we're not suspending, since that means
578
+ // they're missing even though we are out of options for possibly fetching them:
579
+ handlePotentialSnapshotErrorsForState(environment, state);
580
+
581
+ // Ref that stores the epoch of the pending setState, if any. This is used to check
582
+ // if the state we're rendering is at least as current as the pending update, and
583
+ // force a refresh if stale.
584
+ const pendingStateEpochRef = useRef<number | null>(null);
585
+
586
+ // We emulate CRUD effects using a ref and two effects:
587
+ // - The ref tracks the current state (including updates from the subscription)
588
+ // and the dispose function for the current subscription. This is null until
589
+ // a subscription is established.
590
+ // - The first effect is the "update" effect, and re-runs when the environment
591
+ // or state changes. It is responsible for disposing of the previous subscription
592
+ // and establishing a new one, but it manualy reconciles the current state
593
+ // with the subscribed state and bails out if it is already subscribed to the
594
+ // correct (current) state.
595
+ // - The second effect is the mount/unmount (and attach/reattach effect). It
596
+ // makes sure that the subscription is disposed when the component unmounts
597
+ // or detaches (<Activity> going hidden), and then re-subscribes when the component
598
+ // re-attaches (<Activity> going visible). These cases wouldn't fire the
599
+ // "update" effect because the state and environment don't change.
600
+ const storeSubscriptionRef = useRef<{
601
+ dispose: () => void,
602
+ selector: ?ReaderSelector,
603
+ environment: IEnvironment,
604
+ } | null>(null);
605
+ useEffect(() => {
606
+ const storeSubscription = storeSubscriptionRef.current;
607
+ if (storeSubscription != null) {
608
+ if (
609
+ state.environment === storeSubscription.environment &&
610
+ state.selector === storeSubscription.selector
611
+ ) {
612
+ // We're already subscribed to the same selector, so no need to do anything
613
+ return;
614
+ } else {
615
+ // The selector has changed, so we need to dispose of the previous subscription
616
+ storeSubscription.dispose();
617
+ }
618
+ }
619
+ if (state.kind === 'bailout') {
620
+ return;
621
+ }
622
+ // The FragmentState that we'll actually subscribe to. Note that it's possible that
623
+ // a concurrent modification to the store didn't affect the snapshot _data_ (so we don't
624
+ // need to re-render), but did affect the seen records. So if there were missed updates
625
+ // we use that state to subscribe.
626
+ let stateForSubscription: FragmentState = state;
627
+ // No subscription yet or the selector has changed, so we need to subscribe
628
+ // first check for updates since the state was rendered
629
+ const updates = handleMissedUpdates(state.environment, state);
630
+ if (updates !== null) {
631
+ const [didMissUpdates, updatedState] = updates;
632
+ // TODO: didMissUpdates only checks for changes to snapshot data, but it's possible
633
+ // that other snapshot properties may have changed that should also trigger a re-render,
634
+ // such as changed missing resolver fields, missing client edges, etc.
635
+ // A potential alternative is for handleMissedUpdates() to recycle the entire state
636
+ // value, and return the new (recycled) state only if there was some change. In that
637
+ // case the code would always setState if something in the snapshot changed, in addition
638
+ // to using the latest snapshot to subscribe.
639
+ if (didMissUpdates) {
640
+ setState(updatedState);
641
+ // We missed updates, we're going to render again anyway so wait until then to subscribe
642
+ return;
643
+ }
644
+ stateForSubscription = updatedState;
645
+ }
646
+ const dispose = subscribeToSnapshot(
647
+ state.environment,
648
+ stateForSubscription,
649
+ setState,
650
+ pendingStateEpochRef,
651
+ );
652
+ storeSubscriptionRef.current = {
653
+ dispose,
654
+ selector: state.selector,
655
+ environment: state.environment,
656
+ };
657
+ }, [state]);
658
+ useEffect(() => {
659
+ if (storeSubscriptionRef.current == null && state.kind !== 'bailout') {
660
+ const dispose = subscribeToSnapshot(
661
+ state.environment,
662
+ state,
663
+ setState,
664
+ pendingStateEpochRef,
665
+ );
666
+ storeSubscriptionRef.current = {
667
+ dispose,
668
+ selector: state.selector,
669
+ environment: state.environment,
670
+ };
671
+ }
672
+ return () => {
673
+ storeSubscriptionRef.current?.dispose();
674
+ storeSubscriptionRef.current = null;
675
+ };
676
+ // NOTE: this intentionally has no dependencies, see above comment about
677
+ // simulating a CRUD effect
678
+ }, []);
679
+
680
+ // If a low-priority update was queued and hasn't rendered yet, render it now
681
+ if (
682
+ pendingStateEpochRef.current !== null &&
683
+ pendingStateEpochRef.current !== state.epoch
684
+ ) {
685
+ const updates = handleMissedUpdates(environment, state);
686
+ if (updates != null) {
687
+ const [hasStateUpdates, updatedState] = updates;
688
+ if (hasStateUpdates) {
689
+ setState(updatedState);
690
+ state = updatedState;
691
+ }
692
+ }
693
+ }
694
+ // $FlowFixMe[react-rule-unsafe-ref]
695
+ pendingStateEpochRef.current = null;
696
+
697
+ let data: ?SelectorData | Array<?SelectorData>;
698
+ if (isPlural) {
699
+ // Plural fragments require allocating an array of the snapshot data values,
700
+ // which has to be memoized to avoid triggering downstream re-renders.
701
+ //
702
+ // Note that isPlural is a constant property of the fragment and does not change
703
+ // for a particular useFragment invocation site
704
+ const fragmentRefIsNullish = fragmentRef == null; // for less sensitive memoization
705
+ // eslint-disable-next-line react-hooks/rules-of-hooks
706
+ // $FlowFixMe[react-rule-hook]
707
+ data = useMemo(() => {
708
+ if (state.kind === 'bailout') {
709
+ // Bailout state can happen if the fragmentRef is a plural array that is empty or has no
710
+ // non-null entries. In that case, the compatible behavior is to return [] instead of null.
711
+ return fragmentRefIsNullish ? null : [];
712
+ } else {
713
+ invariant(
714
+ state.kind === 'plural',
715
+ 'Expected state to be plural because fragment is plural',
716
+ );
717
+ return state.snapshots.map(s => s.data);
718
+ }
719
+ }, [state, fragmentRefIsNullish]);
720
+ } else if (state.kind === 'bailout') {
721
+ // This case doesn't allocate a new object so it doesn't have to be memoized
722
+ data = null;
723
+ } else {
724
+ // This case doesn't allocate a new object so it doesn't have to be memoized
725
+ invariant(
726
+ state.kind === 'singular',
727
+ 'Expected state to be singular because fragment is singular',
728
+ );
729
+ data = state.snapshot.data;
730
+ }
731
+
732
+ if (RelayFeatureFlags.LOG_MISSING_RECORDS_IN_PROD || __DEV__) {
733
+ if (
734
+ fragmentRef != null &&
735
+ (data === undefined ||
736
+ (Array.isArray(data) &&
737
+ data.length > 0 &&
738
+ data.every(d => d === undefined)))
739
+ ) {
740
+ warning(
741
+ false,
742
+ 'Relay: Expected to have been able to read non-null data for ' +
743
+ 'fragment `%s` declared in ' +
744
+ '`%s`, since fragment reference was non-null. ' +
745
+ "Make sure that that `%s`'s parent isn't " +
746
+ 'holding on to and/or passing a fragment reference for data that ' +
747
+ 'has been deleted.',
748
+ fragmentNode.name,
749
+ hookDisplayName,
750
+ hookDisplayName,
751
+ );
752
+ }
753
+ }
754
+
755
+ if (__DEV__) {
756
+ // eslint-disable-next-line react-hooks/rules-of-hooks
757
+ // $FlowFixMe[react-rule-hook]
758
+ useDebugValue({fragment: fragmentNode.name, data});
759
+ }
760
+
761
+ return data;
762
+ }
763
+
764
+ module.exports = useFragmentInternal_EXPERIMENTAL;