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