react-relay 18.0.0 → 18.2.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 (41) hide show
  1. package/ReactRelayContext.js +1 -1
  2. package/ReactRelayFragmentContainer.js.flow +2 -5
  3. package/buildReactRelayContainer.js.flow +1 -0
  4. package/hooks.js +1 -1
  5. package/index.js +1 -1
  6. package/index.js.flow +3 -0
  7. package/legacy.js +1 -1
  8. package/lib/index.js +2 -0
  9. package/lib/relay-hooks/getConnectionState.js +47 -0
  10. package/lib/relay-hooks/legacy/FragmentResource.js +3 -8
  11. package/lib/relay-hooks/loadQuery.js +5 -14
  12. package/lib/relay-hooks/readFragmentInternal.js +2 -4
  13. package/lib/relay-hooks/useFragmentInternal.js +1 -1
  14. package/lib/relay-hooks/useFragmentInternal_CURRENT.js +3 -10
  15. package/lib/relay-hooks/useFragmentInternal_EXPERIMENTAL.js +6 -28
  16. package/lib/relay-hooks/useLoadMoreFunction.js +10 -43
  17. package/lib/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js +130 -0
  18. package/lib/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js +227 -0
  19. package/lib/relay-hooks/useQueryLoader.js +8 -0
  20. package/lib/relay-hooks/useQueryLoader_EXPERIMENTAL.js +120 -0
  21. package/package.json +2 -2
  22. package/react-relay-hooks.js +2 -2
  23. package/react-relay-hooks.min.js +2 -2
  24. package/react-relay-legacy.js +1 -1
  25. package/react-relay-legacy.min.js +1 -1
  26. package/react-relay.js +2 -2
  27. package/react-relay.min.js +2 -2
  28. package/relay-hooks/EntryPointTypes.flow.js.flow +2 -2
  29. package/relay-hooks/MatchContainer.js.flow +1 -1
  30. package/relay-hooks/getConnectionState.js.flow +97 -0
  31. package/relay-hooks/legacy/FragmentResource.js.flow +4 -16
  32. package/relay-hooks/loadQuery.js.flow +30 -38
  33. package/relay-hooks/readFragmentInternal.js.flow +1 -10
  34. package/relay-hooks/useFragmentInternal.js.flow +1 -1
  35. package/relay-hooks/useFragmentInternal_CURRENT.js.flow +7 -22
  36. package/relay-hooks/useFragmentInternal_EXPERIMENTAL.js.flow +8 -56
  37. package/relay-hooks/useLoadMoreFunction.js.flow +14 -80
  38. package/relay-hooks/useLoadMoreFunction_EXPERIMENTAL.js.flow +280 -0
  39. package/relay-hooks/usePrefetchableForwardPaginationFragment_EXPERIMENTAL.js.flow +433 -0
  40. package/relay-hooks/useQueryLoader.js.flow +27 -3
  41. package/relay-hooks/useQueryLoader_EXPERIMENTAL.js.flow +253 -0
@@ -25,9 +25,10 @@ import type {
25
25
 
26
26
  const {loadQuery} = require('./loadQuery');
27
27
  const useIsMountedRef = require('./useIsMountedRef');
28
+ const useQueryLoader_EXPERIMENTAL = require('./useQueryLoader_EXPERIMENTAL');
28
29
  const useRelayEnvironment = require('./useRelayEnvironment');
29
30
  const {useCallback, useEffect, useRef, useState} = require('react');
30
- const {getRequest} = require('relay-runtime');
31
+ const {RelayFeatureFlags, getRequest} = require('relay-runtime');
31
32
 
32
33
  export type LoaderFn<TQuery: OperationType> = (
33
34
  variables: TQuery['variables'],
@@ -42,7 +43,7 @@ export type UseQueryLoaderLoadQueryOptions = $ReadOnly<{
42
43
  // NullQueryReference needs to implement referential equality,
43
44
  // so that multiple NullQueryReferences can be in the same set
44
45
  // (corresponding to multiple calls to disposeQuery).
45
- type NullQueryReference = {
46
+ export type NullQueryReference = {
46
47
  kind: 'NullQueryReference',
47
48
  };
48
49
  const initialNullQueryReferenceState = {kind: 'NullQueryReference'};
@@ -68,7 +69,7 @@ function requestIsLiveQuery<
68
69
  return request.params.metadata.live !== undefined;
69
70
  }
70
71
 
71
- type UseQueryLoaderHookReturnType<
72
+ export type UseQueryLoaderHookReturnType<
72
73
  TVariables: Variables,
73
74
  TData,
74
75
  TRawResponse: ?{...} = void,
@@ -115,6 +116,29 @@ hook useQueryLoader<TVariables: Variables, TData, TRawResponse: ?{...} = void>(
115
116
  variables: TVariables,
116
117
  rawResponse?: $NonMaybeType<TRawResponse>,
117
118
  }>,
119
+ ): UseQueryLoaderHookReturnType<TVariables, TData> {
120
+ if (RelayFeatureFlags.ENABLE_ACTIVITY_COMPATIBILITY) {
121
+ // $FlowFixMe[react-rule-hook] - the condition is static
122
+ return useQueryLoader_EXPERIMENTAL(
123
+ preloadableRequest,
124
+ initialQueryReference,
125
+ );
126
+ }
127
+ // $FlowFixMe[react-rule-hook] - the condition is static
128
+ return useQueryLoader_CURRENT(preloadableRequest, initialQueryReference);
129
+ }
130
+
131
+ hook useQueryLoader_CURRENT<
132
+ TVariables: Variables,
133
+ TData,
134
+ TRawResponse: ?{...} = void,
135
+ >(
136
+ preloadableRequest: Query<TVariables, TData, TRawResponse>,
137
+ initialQueryReference?: ?PreloadedQuery<{
138
+ response: TData,
139
+ variables: TVariables,
140
+ rawResponse?: $NonMaybeType<TRawResponse>,
141
+ }>,
118
142
  ): UseQueryLoaderHookReturnType<TVariables, TData> {
119
143
  type QueryType = {
120
144
  response: TData,
@@ -0,0 +1,253 @@
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 {
15
+ PreloadableConcreteRequest,
16
+ PreloadedQuery,
17
+ } from './EntryPointTypes.flow';
18
+ import type {
19
+ NullQueryReference,
20
+ UseQueryLoaderHookReturnType,
21
+ UseQueryLoaderLoadQueryOptions,
22
+ } from './useQueryLoader';
23
+ import type {OperationType, Query, Variables} from 'relay-runtime';
24
+
25
+ const {loadQuery} = require('./loadQuery');
26
+ const useIsMountedRef = require('./useIsMountedRef');
27
+ const useRelayEnvironment = require('./useRelayEnvironment');
28
+ const {
29
+ useCallback,
30
+ useEffect,
31
+ // $FlowFixMe[prop-missing] Remove once callers are migrated to official API
32
+ useInsertionEffect,
33
+ useRef,
34
+ useState,
35
+ } = require('react');
36
+ const {getRequest} = require('relay-runtime');
37
+
38
+ const initialNullQueryReferenceState = {kind: 'NullQueryReference'};
39
+
40
+ function requestIsLiveQuery<
41
+ TVariables: Variables,
42
+ TData,
43
+ TRawResponse: ?{...} = void,
44
+ TQuery: OperationType = {
45
+ response: TData,
46
+ variables: TVariables,
47
+ rawResponse?: $NonMaybeType<TRawResponse>,
48
+ },
49
+ >(
50
+ preloadableRequest:
51
+ | Query<TVariables, TData, TRawResponse>
52
+ | PreloadableConcreteRequest<TQuery>,
53
+ ): boolean {
54
+ if (preloadableRequest.kind === 'PreloadableConcreteRequest') {
55
+ return preloadableRequest.params.metadata.live !== undefined;
56
+ }
57
+ const request = getRequest(preloadableRequest);
58
+ return request.params.metadata.live !== undefined;
59
+ }
60
+
61
+ const CLEANUP_TIMEOUT = 1000 * 60 * 5; // 5 minutes;
62
+
63
+ hook useQueryLoader_EXPERIMENTAL<
64
+ TVariables: Variables,
65
+ TData,
66
+ TRawResponse: ?{...} = void,
67
+ >(
68
+ preloadableRequest: Query<TVariables, TData, TRawResponse>,
69
+ initialQueryReference?: ?PreloadedQuery<{
70
+ response: TData,
71
+ variables: TVariables,
72
+ rawResponse?: $NonMaybeType<TRawResponse>,
73
+ }>,
74
+ ): UseQueryLoaderHookReturnType<TVariables, TData> {
75
+ type QueryType = {
76
+ response: TData,
77
+ variables: TVariables,
78
+ rawResponse?: $NonMaybeType<TRawResponse>,
79
+ };
80
+
81
+ /**
82
+ * We want to always call `queryReference.dispose()` for every call to
83
+ * `setQueryReference(loadQuery(...))` so that no leaks of data in Relay stores
84
+ * will occur.
85
+ *
86
+ * However, a call to `setState(newState)` is not always followed by a commit where
87
+ * this value is reflected in the state. Thus, we cannot reliably clean up each
88
+ * ref with `useEffect(() => () => queryReference.dispose(), [queryReference])`.
89
+ *
90
+ * Instead, we keep track of each call to `loadQuery` in a ref.
91
+ * Relying on the fact that if a state change commits, no state changes that were
92
+ * initiated prior to the currently committing state change will ever subsequently
93
+ * commit, we can safely dispose of all preloaded query references
94
+ * associated with state changes initiated prior to the currently committing state
95
+ * change.
96
+ *
97
+ * Finally, when the hook unmounts, we also dispose of all remaining uncommitted
98
+ * query references.
99
+ */
100
+
101
+ const initialQueryReferenceInternal =
102
+ initialQueryReference ?? initialNullQueryReferenceState;
103
+
104
+ const environment = useRelayEnvironment();
105
+
106
+ const isMountedRef = useIsMountedRef();
107
+ const undisposedQueryReferencesRef = useRef<Set<
108
+ PreloadedQuery<QueryType> | NullQueryReference,
109
+ > | null>(null);
110
+ if (undisposedQueryReferencesRef.current == null) {
111
+ undisposedQueryReferencesRef.current = new Set([
112
+ initialQueryReferenceInternal,
113
+ ]);
114
+ }
115
+
116
+ const [queryReference, setQueryReference] = useState<
117
+ PreloadedQuery<QueryType> | NullQueryReference,
118
+ >(() => initialQueryReferenceInternal);
119
+
120
+ const [previousInitialQueryReference, setPreviousInitialQueryReference] =
121
+ useState<PreloadedQuery<QueryType> | NullQueryReference>(
122
+ () => initialQueryReferenceInternal,
123
+ );
124
+
125
+ if (initialQueryReferenceInternal !== previousInitialQueryReference) {
126
+ // Rendering the query reference makes it "managed" by this hook, so
127
+ // we start keeping track of it so we can dispose it when it is no longer
128
+ // necessary here
129
+ // TODO(T78446637): Handle disposal of managed query references in
130
+ // components that were never mounted after rendering
131
+ // $FlowFixMe[react-rule-unsafe-ref]
132
+ undisposedQueryReferencesRef.current?.add(initialQueryReferenceInternal);
133
+ setPreviousInitialQueryReference(initialQueryReferenceInternal);
134
+ setQueryReference(initialQueryReferenceInternal);
135
+ }
136
+
137
+ const disposeQuery = useCallback(() => {
138
+ if (isMountedRef.current) {
139
+ undisposedQueryReferencesRef.current?.add(initialNullQueryReferenceState);
140
+ setQueryReference(initialNullQueryReferenceState);
141
+ }
142
+ }, [isMountedRef]);
143
+
144
+ const queryLoaderCallback = useCallback(
145
+ (variables: TVariables, options?: ?UseQueryLoaderLoadQueryOptions) => {
146
+ if (!isMountedRef.current) {
147
+ return;
148
+ }
149
+ const mergedOptions: ?UseQueryLoaderLoadQueryOptions =
150
+ options != null && options.hasOwnProperty('__environment')
151
+ ? {
152
+ fetchPolicy: options.fetchPolicy,
153
+ networkCacheConfig: options.networkCacheConfig,
154
+ __nameForWarning: options.__nameForWarning,
155
+ }
156
+ : options;
157
+ const updatedQueryReference = loadQuery(
158
+ options?.__environment ?? environment,
159
+ preloadableRequest,
160
+ variables,
161
+ (mergedOptions: $FlowFixMe),
162
+ );
163
+ undisposedQueryReferencesRef.current?.add(updatedQueryReference);
164
+ setQueryReference(updatedQueryReference);
165
+ },
166
+ [environment, preloadableRequest, setQueryReference, isMountedRef],
167
+ );
168
+
169
+ const disposeAllRemainingQueryReferences = useCallback(
170
+ function disposeAllRemainingQueryReferences(
171
+ preloadableRequest: Query<TVariables, TData, TRawResponse>,
172
+ currentQueryReference:
173
+ | PreloadedQuery<QueryType>
174
+ | NullQueryReference
175
+ | null,
176
+ ) {
177
+ const undisposedQueryReferences =
178
+ undisposedQueryReferencesRef.current ?? new Set();
179
+ // undisposedQueryReferences.current is never reassigned
180
+ // eslint-disable-next-line react-hooks/exhaustive-deps
181
+ for (const undisposedQueryReference of undisposedQueryReferences) {
182
+ if (undisposedQueryReference === currentQueryReference) {
183
+ continue;
184
+ }
185
+ if (undisposedQueryReference.kind !== 'NullQueryReference') {
186
+ if (requestIsLiveQuery(preloadableRequest)) {
187
+ undisposedQueryReference.dispose &&
188
+ undisposedQueryReference.dispose();
189
+ } else {
190
+ undisposedQueryReference.releaseQuery &&
191
+ undisposedQueryReference.releaseQuery();
192
+ }
193
+ }
194
+ }
195
+ },
196
+ [],
197
+ );
198
+
199
+ const cleanupTimerRef = useRef<?TimeoutID>(null);
200
+ useEffect(() => {
201
+ // When a new queryReference is committed, we iterate over all
202
+ // query references in undisposedQueryReferences and dispose all of
203
+ // the refs that aren't the currently committed one. This ensures
204
+ // that we don't leave any dangling query references for the
205
+ // case that loadQuery is called multiple times before commit; when
206
+ // this happens, multiple state updates will be scheduled, but only one
207
+ // will commit, meaning that we need to keep track of and dispose any
208
+ // query references that don't end up committing.
209
+ // - We are relying on the fact that sets iterate in insertion order, and we
210
+ // can remove items from a set as we iterate over it (i.e. no iterator
211
+ // invalidation issues.) Thus, it is safe to loop through
212
+ // undisposedQueryReferences until we find queryReference, and
213
+ // remove and dispose all previous references.
214
+ // - We are guaranteed to find queryReference in the set, because if a
215
+ // state update results in a commit, no state updates initiated prior to that
216
+ // one will be committed, and we are disposing and removing references
217
+ // associated with updates that were scheduled prior to the currently
218
+ // committing state change. (A useEffect callback is called during the commit
219
+ // phase.)
220
+ disposeAllRemainingQueryReferences(preloadableRequest, queryReference);
221
+ if (cleanupTimerRef.current != null) {
222
+ clearTimeout(cleanupTimerRef.current);
223
+ cleanupTimerRef.current = null;
224
+ }
225
+ return () => {
226
+ cleanupTimerRef.current = setTimeout(() => {
227
+ disposeAllRemainingQueryReferences(preloadableRequest, null);
228
+ }, CLEANUP_TIMEOUT);
229
+ };
230
+ }, [preloadableRequest, queryReference]);
231
+
232
+ // $FlowFixMe[not-a-function]
233
+ useInsertionEffect(() => {
234
+ // We use an insertion effect to ensure that we cleanup the final query
235
+ // reference when the component truly unmounts. Note that a regular
236
+ // useEffect may detach/reattach due to <Activity> multiple times, and
237
+ // we don't want to free queries that may be used when the component reveals
238
+ // again.
239
+ return () => {
240
+ cleanupTimerRef.current && clearTimeout(cleanupTimerRef.current);
241
+ cleanupTimerRef.current = null;
242
+ disposeAllRemainingQueryReferences(preloadableRequest, null);
243
+ };
244
+ }, [preloadableRequest]);
245
+
246
+ return [
247
+ queryReference.kind === 'NullQueryReference' ? null : queryReference,
248
+ queryLoaderCallback,
249
+ disposeQuery,
250
+ ];
251
+ }
252
+
253
+ module.exports = useQueryLoader_EXPERIMENTAL;