react-relay 17.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;