react-relay 19.0.0 → 20.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Relay v19.0.0
2
+ * Relay v20.0.0
3
3
  *
4
4
  * Copyright (c) Meta Platforms, Inc. and affiliates.
5
5
  *
@@ -82,8 +82,7 @@ function buildReactRelayContainer<TBase: component(...empty)>(
82
82
  );
83
83
  }
84
84
  ForwardRef.displayName = containerName;
85
- // $FlowFixMe[incompatible-call]
86
- const ForwardContainer = React.forwardRef(ForwardRef);
85
+ const ForwardContainer = (React as $FlowFixMe).forwardRef(ForwardRef);
87
86
 
88
87
  if (__DEV__) {
89
88
  // Used by RelayModernTestUtils
package/hooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Relay v19.0.0
2
+ * Relay v20.0.0
3
3
  *
4
4
  * Copyright (c) Meta Platforms, Inc. and affiliates.
5
5
  *
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Relay v19.0.0
2
+ * Relay v20.0.0
3
3
  *
4
4
  * Copyright (c) Meta Platforms, Inc. and affiliates.
5
5
  *
package/legacy.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Relay v19.0.0
2
+ * Relay v20.0.0
3
3
  *
4
4
  * Copyright (c) Meta Platforms, Inc. and affiliates.
5
5
  *
@@ -130,7 +130,7 @@ function readFragmentInternal(environment, fragmentNode, fragmentRef, hookDispla
130
130
  !(fragmentRef == null || isPlural && Array.isArray(fragmentRef) && fragmentRef.length === 0 || fragmentSelector != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected to receive an object where `...%s` was spread, ' + 'but the fragment reference was not found`. This is most ' + 'likely the result of:\n' + "- Forgetting to spread `%s` in `%s`'s parent's fragment.\n" + '- Conditionally fetching `%s` but unconditionally passing %s prop ' + 'to `%s`. If the parent fragment only fetches the fragment conditionally ' + '- with e.g. `@include`, `@skip`, or inside a `... on SomeType { }` ' + 'spread - then the fragment reference will not exist. ' + 'In this case, pass `null` if the conditions for evaluating the ' + 'fragment are not met (e.g. if the `@include(if)` value is false.)', fragmentNode.name, fragmentNode.name, hookDisplayName, fragmentNode.name, fragmentKey == null ? 'a fragment reference' : "the `".concat(fragmentKey, "`"), hookDisplayName) : invariant(false) : void 0;
131
131
  var state = getFragmentState(environment, fragmentSelector);
132
132
  var clientEdgeQueries = null;
133
- if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true) {
133
+ if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true || RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES) {
134
134
  var missingClientEdges = getMissingClientEdges(state);
135
135
  if (missingClientEdges !== null && missingClientEdges !== void 0 && missingClientEdges.length) {
136
136
  clientEdgeQueries = [];
@@ -343,7 +343,7 @@ function useFragmentInternal(fragmentNode, fragmentRef, hookDisplayName, queryOp
343
343
  useEffect(function () {
344
344
  committedFragmentSelectorRef.current = fragmentSelector;
345
345
  }, [fragmentSelector]);
346
- if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true) {
346
+ if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true || RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES) {
347
347
  var _useMemo = useMemo(function () {
348
348
  var missingClientEdges = getMissingClientEdges(state);
349
349
  var clientEdgeQueries;
@@ -199,7 +199,11 @@ function handleMissingClientEdge(environment, parentFragmentNode, parentFragment
199
199
  var queryOperationDescriptor = createOperationDescriptor(missingClientEdgeRequestInfo.request, variables, queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.networkCacheConfig);
200
200
  var QueryResource = getQueryResourceForEnvironment(environment);
201
201
  var queryResult = QueryResource.prepare(queryOperationDescriptor, fetchQueryInternal(environment, queryOperationDescriptor), queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.fetchPolicy);
202
- return [queryResult, getPromiseForActiveRequest(environment, queryOperationDescriptor.request)];
202
+ var promise = getPromiseForActiveRequest(environment, queryOperationDescriptor.request);
203
+ if (promise != null && promise.displayName == null) {
204
+ promise.displayName = missingClientEdgeRequestInfo.request.params.name;
205
+ }
206
+ return [queryResult, promise];
203
207
  }
204
208
  function subscribeToSnapshot(environment, state, setState) {
205
209
  if (state.kind === 'bailout') {
@@ -345,7 +349,7 @@ function useFragmentInternal_EXPERIMENTAL(fragmentNode, fragmentRef, hookDisplay
345
349
  useEffect(function () {
346
350
  committedFragmentSelectorRef.current = fragmentSelector;
347
351
  }, [fragmentSelector]);
348
- if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true) {
352
+ if (((_fragmentNode$metadat2 = fragmentNode.metadata) === null || _fragmentNode$metadat2 === void 0 ? void 0 : _fragmentNode$metadat2.hasClientEdges) === true || RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES) {
349
353
  var _useMemo = useMemo(function () {
350
354
  var missingClientEdges = getMissingClientEdges(state);
351
355
  var clientEdgeQueries;
@@ -376,7 +380,11 @@ function useFragmentInternal_EXPERIMENTAL(fragmentNode, fragmentRef, hookDisplay
376
380
  clientEdgeQueries = _useMemo[0],
377
381
  activeRequestPromises = _useMemo[1];
378
382
  if (activeRequestPromises.length) {
379
- throw Promise.all(activeRequestPromises);
383
+ var allPromises = Promise.all(activeRequestPromises);
384
+ allPromises.displayName = "RelayClientEdge(".concat(activeRequestPromises.map(function (promise) {
385
+ return promise.displayName;
386
+ }).join(','), ")");
387
+ throw allPromises;
380
388
  }
381
389
  useEffect(function () {
382
390
  var QueryResource = getQueryResourceForEnvironment(environment);
@@ -406,9 +414,11 @@ function useFragmentInternal_EXPERIMENTAL(fragmentNode, fragmentRef, hookDisplay
406
414
  if (isMissingData(state)) {
407
415
  var suspendingLiveResolvers = getSuspendingLiveResolver(state);
408
416
  if (suspendingLiveResolvers != null && suspendingLiveResolvers.length > 0) {
409
- throw Promise.all(suspendingLiveResolvers.map(function (liveStateID) {
417
+ var promise = Promise.all(suspendingLiveResolvers.map(function (liveStateID) {
410
418
  return environment.getStore().getLiveResolverPromise(liveStateID);
411
419
  }));
420
+ promise.displayName = 'RelayLiveResolver(' + fragmentNode.name + ')';
421
+ throw promise;
412
422
  }
413
423
  if (RelayFeatureFlags.ENABLE_RELAY_OPERATION_TRACKER_SUSPENSE || environment !== previousEnvironment || !committedFragmentSelectorRef.current || !areEqualSelectors(committedFragmentSelectorRef.current, fragmentSelector)) {
414
424
  !(fragmentSelector != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'refinement, see invariants above') : invariant(false) : void 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-relay",
3
3
  "description": "A framework for building GraphQL-driven React applications.",
4
- "version": "19.0.0",
4
+ "version": "20.0.0",
5
5
  "keywords": [
6
6
  "graphql",
7
7
  "relay",
@@ -20,7 +20,7 @@
20
20
  "fbjs": "^3.0.2",
21
21
  "invariant": "^2.2.4",
22
22
  "nullthrows": "^1.1.1",
23
- "relay-runtime": "19.0.0"
23
+ "relay-runtime": "20.0.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "react": "^16.9.0 || ^17 || ^18 || ^19"
@@ -37,6 +37,7 @@ export type PreloadOptions = {
37
37
  +fetchKey?: string | number,
38
38
  +fetchPolicy?: ?PreloadFetchPolicy,
39
39
  +includeIf?: ?boolean,
40
+ +prefetchExpiryInHours?: ?number,
40
41
  +networkCacheConfig?: ?CacheConfig,
41
42
  };
42
43
 
@@ -43,7 +43,7 @@ const {
43
43
 
44
44
  let fetchKey = 100001;
45
45
 
46
- type QueryType<T> =
46
+ export type QueryType<T> =
47
47
  T extends Query<infer V, infer D, infer RR>
48
48
  ? {
49
49
  variables: V,
@@ -126,7 +126,7 @@ function loadQuery<
126
126
  let networkError = null;
127
127
  // makeNetworkRequest will immediately start a raw network request if
128
128
  // one isn't already in flight and return an Observable that when
129
- // subscribed to will replay the network events that have occured so far,
129
+ // subscribed to will replay the network events that have occurred so far,
130
130
  // as well as subsequent events.
131
131
  let didMakeNetworkRequest = false;
132
132
  const makeNetworkRequest = (
@@ -213,7 +213,10 @@ function readFragmentInternal(
213
213
  // Handle the queries for any missing client edges; this may suspend.
214
214
  // FIXME handle client edges in parallel.
215
215
  let clientEdgeQueries = null;
216
- if (fragmentNode.metadata?.hasClientEdges === true) {
216
+ if (
217
+ fragmentNode.metadata?.hasClientEdges === true ||
218
+ RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES
219
+ ) {
217
220
  const missingClientEdges = getMissingClientEdges(state);
218
221
  if (missingClientEdges?.length) {
219
222
  clientEdgeQueries = ([]: Array<QueryResult>);
@@ -158,7 +158,7 @@ hook useLoadEntryPoint<
158
158
  useEffect(() => {
159
159
  return () => {
160
160
  // Attempt to detect if the component was
161
- // hidden (by Offscreen API), or fast refresh occured;
161
+ // hidden (by Offscreen API), or fast refresh occurred;
162
162
  // Only in these situations would the effect cleanup
163
163
  // for "unmounting" run multiple times, so if
164
164
  // we are ever able to read this ref with a value
@@ -461,7 +461,10 @@ hook useFragmentInternal(
461
461
 
462
462
  // Handle the queries for any missing client edges; this may suspend.
463
463
  // FIXME handle client edges in parallel.
464
- if (fragmentNode.metadata?.hasClientEdges === true) {
464
+ if (
465
+ fragmentNode.metadata?.hasClientEdges === true ||
466
+ RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES
467
+ ) {
465
468
  // The fragment is validated to be static (in useFragment) and hasClientEdges is
466
469
  // a static (constant) property of the fragment. In practice, this effect will
467
470
  // always or never run for a given invocation of this hook.
@@ -222,13 +222,15 @@ function handleMissedUpdates(
222
222
  }
223
223
  }
224
224
 
225
+ type PromiseWithDisplayName = Promise<mixed> & {displayName?: string};
226
+
225
227
  function handleMissingClientEdge(
226
228
  environment: IEnvironment,
227
229
  parentFragmentNode: ReaderFragment,
228
230
  parentFragmentRef: mixed,
229
231
  missingClientEdgeRequestInfo: MissingClientEdgeRequestInfo,
230
232
  queryOptions?: FragmentQueryOptions,
231
- ): [QueryResult, ?Promise<mixed>] {
233
+ ): [QueryResult, ?PromiseWithDisplayName] {
232
234
  const originalVariables = getVariablesFromFragment(
233
235
  parentFragmentNode,
234
236
  parentFragmentRef,
@@ -253,10 +255,17 @@ function handleMissingClientEdge(
253
255
  queryOptions?.fetchPolicy,
254
256
  );
255
257
 
256
- return [
257
- queryResult,
258
- getPromiseForActiveRequest(environment, queryOperationDescriptor.request),
259
- ];
258
+ const promise = getPromiseForActiveRequest(
259
+ environment,
260
+ queryOperationDescriptor.request,
261
+ );
262
+ // $FlowExpectedError[prop-missing]
263
+ if (promise != null && promise.displayName == null) {
264
+ // $FlowExpectedError[prop-missing]
265
+ promise.displayName = missingClientEdgeRequestInfo.request.params.name;
266
+ }
267
+ // $FlowFixMe[incompatible-exact] - Intentionally bypassing exactness check
268
+ return [queryResult, promise];
260
269
  }
261
270
 
262
271
  function subscribeToSnapshot(
@@ -476,7 +485,10 @@ hook useFragmentInternal_EXPERIMENTAL(
476
485
 
477
486
  // Handle the queries for any missing client edges; this may suspend.
478
487
  // FIXME handle client edges in parallel.
479
- if (fragmentNode.metadata?.hasClientEdges === true) {
488
+ if (
489
+ fragmentNode.metadata?.hasClientEdges === true ||
490
+ RelayFeatureFlags.CHECK_ALL_FRAGMENTS_FOR_MISSING_CLIENT_EDGES
491
+ ) {
480
492
  // The fragment is validated to be static (in useFragment) and hasClientEdges is
481
493
  // a static (constant) property of the fragment. In practice, this effect will
482
494
  // always or never run for a given invocation of this hook.
@@ -487,7 +499,7 @@ hook useFragmentInternal_EXPERIMENTAL(
487
499
  const missingClientEdges = getMissingClientEdges(state);
488
500
  // eslint-disable-next-line no-shadow
489
501
  let clientEdgeQueries;
490
- const activeRequestPromises = [];
502
+ const activeRequestPromises: Array<PromiseWithDisplayName> = [];
491
503
  if (missingClientEdges?.length) {
492
504
  clientEdgeQueries = ([]: Array<QueryResult>);
493
505
  for (const edge of missingClientEdges) {
@@ -508,7 +520,12 @@ hook useFragmentInternal_EXPERIMENTAL(
508
520
  }, [state, environment, fragmentNode, fragmentRef, queryOptions]);
509
521
 
510
522
  if (activeRequestPromises.length) {
511
- throw Promise.all(activeRequestPromises);
523
+ const allPromises = Promise.all(activeRequestPromises);
524
+ // $FlowExpectedError[prop-missing] Expando to annotate Promises.
525
+ allPromises.displayName = `RelayClientEdge(${activeRequestPromises
526
+ .map(promise => promise.displayName)
527
+ .join(',')})`;
528
+ throw allPromises;
512
529
  }
513
530
 
514
531
  // See above note
@@ -535,12 +552,15 @@ hook useFragmentInternal_EXPERIMENTAL(
535
552
  // Suspend if a Live Resolver within this fragment is in a suspended state:
536
553
  const suspendingLiveResolvers = getSuspendingLiveResolver(state);
537
554
  if (suspendingLiveResolvers != null && suspendingLiveResolvers.length > 0) {
538
- throw Promise.all(
555
+ const promise = Promise.all(
539
556
  suspendingLiveResolvers.map(liveStateID => {
540
557
  // $FlowFixMe[prop-missing] This is expected to be a RelayModernStore
541
558
  return environment.getStore().getLiveResolverPromise(liveStateID);
542
559
  }),
543
560
  );
561
+ // $FlowExpectedError[prop-missing] Expando to annotate Promises.
562
+ promise.displayName = 'RelayLiveResolver(' + fragmentNode.name + ')';
563
+ throw promise;
544
564
  }
545
565
  // Suspend if an active operation bears on this fragment, either the
546
566
  // fragment's owner or some other mutation etc. that could affect it.
@@ -32,23 +32,72 @@ const {
32
32
  export type UseLazyLoadQueryHookType = hook <TVariables: Variables, TData>(
33
33
  gqlQuery: Query<TVariables, TData>,
34
34
  variables: TVariables,
35
- options?: $ReadOnly<{
36
- fetchKey?: string | number,
37
- fetchPolicy?: FetchPolicy,
38
- networkCacheConfig?: CacheConfig,
39
- UNSTABLE_renderPolicy?: RenderPolicy,
40
- }>,
35
+ options?: Options,
41
36
  ) => TData;
42
37
 
38
+ type Options = {
39
+ /**
40
+ * Determines if cached data should be used, and when to send a network request based on the cached data that is currently available in the Relay store (for more details, see our [Fetch Policies](../../guided-tour/reusing-cached-data/fetch-policies) and [Garbage Collection](../../guided-tour/reusing-cached-data/presence-of-data) guides):
41
+ * * "store-or-network": _*(default)*_ *will* reuse locally cached data and will *only* send a network request if any data for the query is missing. If the query is fully cached, a network request will *not* be made.
42
+ * * "store-and-network": *will* reuse locally cached data and will *always* send a network request, regardless of whether any data was missing from the local cache or not.
43
+ * * "network-only": *will* *not* reuse locally cached data, and will *always* send a network request to fetch the query, ignoring any data that might be locally cached in Relay.
44
+ * * "store-only": *will* *only* reuse locally cached data, and will *never* send a network request to fetch the query. In this case, the responsibility of fetching the query falls to the caller, but this policy could also be used to read and operate on data that is entirely [local](../../guided-tour/updating-data/local-data-updates).
45
+ */
46
+ +fetchPolicy?: FetchPolicy,
47
+ /**
48
+ * A `fetchKey` can be passed to force a re-evaluation of the current query and variables when the component re-renders, even if the variables didn't change, or even if the component isn't remounted (similarly to how passing a different `key` to a React component will cause it to remount). If the `fetchKey` is different from the one used in the previous render, the current query will be re-evaluated against the store, and it might be refetched depending on the current `fetchPolicy` and the state of the cache.
49
+ */
50
+ +fetchKey?: string | number,
51
+ /**
52
+ * Default value: `{force: true}`. Object containing cache config options for the *network layer*. Note that the network layer may contain an *additional* query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely (which is the default behavior), pass `{force: true}` as the value for this option.
53
+ */
54
+ +networkCacheConfig?: CacheConfig,
55
+ /**
56
+ * Undocumented option.
57
+ */
58
+ +UNSTABLE_renderPolicy?: RenderPolicy,
59
+ };
60
+
61
+ /**
62
+ * Hook used to fetch a GraphQL query during render. This hook can trigger multiple nested or waterfalling round trips if used without caution, and waits until render to start a data fetch (when it can usually start a lot sooner than render), thereby degrading performance. Instead, prefer [`usePreloadedQuery`](../use-preloaded-query).
63
+ *
64
+ * @example
65
+ * const React = require('React');
66
+ *
67
+ * const {graphql, useLazyLoadQuery} = require('react-relay');
68
+ *
69
+ * function App() {
70
+ * const data = useLazyLoadQuery(
71
+ * graphql`
72
+ * query AppQuery($id: ID!) {
73
+ * user(id: $id) {
74
+ * name
75
+ * }
76
+ * }
77
+ * `,
78
+ * {id: 4},
79
+ * {fetchPolicy: 'store-or-network'},
80
+ * );
81
+ *
82
+ * return <h1>{data.user?.name}</h1>;
83
+ * }
84
+ *
85
+ * @returns - `data`: Object that contains data which has been read out from the Relay store; the object matches the shape of specified query.
86
+ * - The Flow type for data will also match this shape, and contain types derived from the GraphQL Schema. For example, the type of `data` above is: `{| user: ?{| name: ?string |} |}`.
87
+ */
43
88
  hook useLazyLoadQuery<TVariables: Variables, TData>(
89
+ /**
90
+ * GraphQL query specified using a `graphql` template literal.
91
+ */
44
92
  gqlQuery: Query<TVariables, TData>,
93
+ /**
94
+ * Object containing the variable values to fetch the query. These variables need to match GraphQL variables declared inside the query.
95
+ */
45
96
  variables: TVariables,
46
- options?: $ReadOnly<{
47
- fetchKey?: string | number,
48
- fetchPolicy?: FetchPolicy,
49
- networkCacheConfig?: CacheConfig,
50
- UNSTABLE_renderPolicy?: RenderPolicy,
51
- }>,
97
+ /**
98
+ * options object
99
+ */
100
+ options?: Options,
52
101
  ): TData {
53
102
  const environment = useRelayEnvironment();
54
103
 
@@ -71,4 +120,4 @@ hook useLazyLoadQuery<TVariables: Variables, TData>(
71
120
  }
72
121
 
73
122
  // $FlowFixMe[react-rule-hook-incompatible]
74
- module.exports = (useLazyLoadQuery: UseLazyLoadQueryHookType);
123
+ module.exports = useLazyLoadQuery;
@@ -78,7 +78,7 @@ hook useLazyLoadQueryNode<TQuery: OperationType>({
78
78
  useEffect(() => {
79
79
  return () => {
80
80
  // Attempt to detect if the component was
81
- // hidden (by Offscreen API), or fast refresh occured;
81
+ // hidden (by Offscreen API), or fast refresh occurred;
82
82
  // Only in these situations would the effect cleanup
83
83
  // for "unmounting" run multiple times, so if
84
84
  // we are ever able to read this ref with a value
@@ -126,7 +126,7 @@ hook usePreloadedQuery<
126
126
  // context, we cannot re-use the existing preloaded query.
127
127
  // Instead, we need to fall back and re-execute and de-dupe the query with
128
128
  // the new environment (at render time).
129
- // TODO T68036756 track occurences of this warning and turn it into a hard error
129
+ // TODO T68036756 track occurrences of this warning and turn it into a hard error
130
130
  warning(
131
131
  false,
132
132
  'usePreloadedQuery(): usePreloadedQuery was passed a preloaded query ' +
@@ -236,7 +236,7 @@ hook useQueryLoader_CURRENT<
236
236
  useEffect(() => {
237
237
  return () => {
238
238
  // Attempt to detect if the component was
239
- // hidden (by Offscreen API), or fast refresh occured;
239
+ // hidden (by Offscreen API), or fast refresh occurred;
240
240
  // Only in these situations would the effect cleanup
241
241
  // for "unmounting" run multiple times, so if
242
242
  // we are ever able to read this ref with a value
@@ -17,6 +17,28 @@ const ReactRelayContext = require('./../ReactRelayContext');
17
17
  const invariant = require('invariant');
18
18
  const {useContext} = require('react');
19
19
 
20
+ /**
21
+ * Hook used to access a Relay environment that was set by a [`RelayEnvironmentProvider`](../relay-environment-provider):
22
+ *
23
+ * @example
24
+ * const React = require('React');
25
+ *
26
+ * const {useRelayEnvironment} = require('react-relay');
27
+ *
28
+ * function MyComponent() {
29
+ * const environment = useRelayEnvironment();
30
+ *
31
+ * const handler = useCallback(() => {
32
+ * // For example, can be used to pass the environment to functions
33
+ * // that require a Relay environment.
34
+ * commitMutation(environment, ...);
35
+ * }, [environment])
36
+ *
37
+ * return (...);
38
+ * }
39
+ *
40
+ * module.exports = MyComponent;
41
+ */
20
42
  hook useRelayEnvironment(): IEnvironment {
21
43
  const context = useContext(ReactRelayContext);
22
44
  invariant(