-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
Copy pathuseQueryLoader.js
332 lines (302 loc) · 11.9 KB
/
useQueryLoader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall relay
*/
'use strict';
import type {
LoadQueryOptions,
PreloadableConcreteRequest,
PreloadedQuery,
} from './EntryPointTypes.flow';
import type {
IEnvironment,
OperationType,
Query,
Variables,
} from 'relay-runtime';
const {loadQuery} = require('./loadQuery');
const useIsMountedRef = require('./useIsMountedRef');
const useQueryLoader_EXPERIMENTAL = require('./useQueryLoader_EXPERIMENTAL');
const useRelayEnvironment = require('./useRelayEnvironment');
const {useCallback, useEffect, useRef, useState} = require('react');
const {RelayFeatureFlags, getRequest} = require('relay-runtime');
export type LoaderFn<TQuery: OperationType> = (
variables: TQuery['variables'],
options?: UseQueryLoaderLoadQueryOptions,
) => void;
export type UseQueryLoaderLoadQueryOptions = $ReadOnly<{
...LoadQueryOptions,
+__environment?: ?IEnvironment,
}>;
// NullQueryReference needs to implement referential equality,
// so that multiple NullQueryReferences can be in the same set
// (corresponding to multiple calls to disposeQuery).
export type NullQueryReference = {
kind: 'NullQueryReference',
};
const initialNullQueryReferenceState = {kind: 'NullQueryReference'};
function requestIsLiveQuery<
TVariables: Variables,
TData,
TRawResponse: ?{...} = void,
TQuery: OperationType = {
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
},
>(
preloadableRequest:
| Query<TVariables, TData, TRawResponse>
| PreloadableConcreteRequest<TQuery>,
): boolean {
if (preloadableRequest.kind === 'PreloadableConcreteRequest') {
return preloadableRequest.params.metadata.live !== undefined;
}
const request = getRequest(preloadableRequest);
return request.params.metadata.live !== undefined;
}
export type UseQueryLoaderHookReturnType<
TVariables: Variables,
TData,
TRawResponse: ?{...} = void,
> = [
?PreloadedQuery<{
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
}>,
(variables: TVariables, options?: UseQueryLoaderLoadQueryOptions) => void,
() => void,
];
declare function useQueryLoader<
TVariables: Variables,
TData,
TRawResponse: ?{...} = void,
>(
preloadableRequest: Query<TVariables, TData, TRawResponse>,
): UseQueryLoaderHookReturnType<TVariables, TData>;
declare function useQueryLoader<
TVariables: Variables,
TData,
TRawResponse: ?{...} = void,
>(
preloadableRequest: Query<TVariables, TData, TRawResponse>,
initialQueryReference: ?PreloadedQuery<{
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
}>,
): UseQueryLoaderHookReturnType<TVariables, TData>;
declare function useQueryLoader<TQuery: OperationType>(
preloadableRequest: PreloadableConcreteRequest<TQuery>,
initialQueryReference?: ?PreloadedQuery<TQuery>,
): UseQueryLoaderHookReturnType<TQuery['variables'], TQuery['response']>;
hook useQueryLoader<TVariables: Variables, TData, TRawResponse: ?{...} = void>(
preloadableRequest: Query<TVariables, TData, TRawResponse>,
initialQueryReference?: ?PreloadedQuery<{
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
}>,
): UseQueryLoaderHookReturnType<TVariables, TData> {
if (RelayFeatureFlags.ENABLE_ACTIVITY_COMPATIBILITY) {
// $FlowFixMe[react-rule-hook] - the condition is static
return useQueryLoader_EXPERIMENTAL(
preloadableRequest,
initialQueryReference,
);
}
// $FlowFixMe[react-rule-hook] - the condition is static
return useQueryLoader_CURRENT(preloadableRequest, initialQueryReference);
}
hook useQueryLoader_CURRENT<
TVariables: Variables,
TData,
TRawResponse: ?{...} = void,
>(
preloadableRequest: Query<TVariables, TData, TRawResponse>,
initialQueryReference?: ?PreloadedQuery<{
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
}>,
): UseQueryLoaderHookReturnType<TVariables, TData> {
type QueryType = {
response: TData,
variables: TVariables,
rawResponse?: $NonMaybeType<TRawResponse>,
};
/**
* We want to always call `queryReference.dispose()` for every call to
* `setQueryReference(loadQuery(...))` so that no leaks of data in Relay stores
* will occur.
*
* However, a call to `setState(newState)` is not always followed by a commit where
* this value is reflected in the state. Thus, we cannot reliably clean up each
* ref with `useEffect(() => () => queryReference.dispose(), [queryReference])`.
*
* Instead, we keep track of each call to `loadQuery` in a ref.
* Relying on the fact that if a state change commits, no state changes that were
* initiated prior to the currently committing state change will ever subsequently
* commit, we can safely dispose of all preloaded query references
* associated with state changes initiated prior to the currently committing state
* change.
*
* Finally, when the hook unmounts, we also dispose of all remaining uncommitted
* query references.
*/
const initialQueryReferenceInternal =
initialQueryReference ?? initialNullQueryReferenceState;
const environment = useRelayEnvironment();
const isMountedRef = useIsMountedRef();
const undisposedQueryReferencesRef = useRef<
Set<PreloadedQuery<QueryType> | NullQueryReference>,
>(new Set([initialQueryReferenceInternal]));
const [queryReference, setQueryReference] = useState<
PreloadedQuery<QueryType> | NullQueryReference,
>(() => initialQueryReferenceInternal);
const [previousInitialQueryReference, setPreviousInitialQueryReference] =
useState<PreloadedQuery<QueryType> | NullQueryReference>(
() => initialQueryReferenceInternal,
);
if (initialQueryReferenceInternal !== previousInitialQueryReference) {
// Rendering the query reference makes it "managed" by this hook, so
// we start keeping track of it so we can dispose it when it is no longer
// necessary here
// TODO(T78446637): Handle disposal of managed query references in
// components that were never mounted after rendering
// $FlowFixMe[react-rule-unsafe-ref]
undisposedQueryReferencesRef.current.add(initialQueryReferenceInternal);
setPreviousInitialQueryReference(initialQueryReferenceInternal);
setQueryReference(initialQueryReferenceInternal);
}
const disposeQuery = useCallback(() => {
if (isMountedRef.current) {
undisposedQueryReferencesRef.current.add(initialNullQueryReferenceState);
setQueryReference(initialNullQueryReferenceState);
}
}, [isMountedRef]);
const queryLoaderCallback = useCallback(
(variables: TVariables, options?: ?UseQueryLoaderLoadQueryOptions) => {
const mergedOptions: ?UseQueryLoaderLoadQueryOptions =
options != null && options.hasOwnProperty('__environment')
? {
fetchPolicy: options.fetchPolicy,
networkCacheConfig: options.networkCacheConfig,
__nameForWarning: options.__nameForWarning,
}
: options;
if (isMountedRef.current) {
const updatedQueryReference = loadQuery(
options?.__environment ?? environment,
preloadableRequest,
variables,
(mergedOptions: $FlowFixMe),
);
undisposedQueryReferencesRef.current.add(updatedQueryReference);
setQueryReference(updatedQueryReference);
}
},
[environment, preloadableRequest, setQueryReference, isMountedRef],
);
const maybeHiddenOrFastRefresh = useRef(false);
useEffect(() => {
return () => {
// Attempt to detect if the component was
// hidden (by Offscreen API), or fast refresh occured;
// Only in these situations would the effect cleanup
// for "unmounting" run multiple times, so if
// we are ever able to read this ref with a value
// of true, it means that one of these cases
// has happened.
maybeHiddenOrFastRefresh.current = true;
};
}, []);
useEffect(() => {
if (maybeHiddenOrFastRefresh.current === true) {
// This block only runs if the component has previously "unmounted"
// due to it being hidden by the Offscreen API, or during fast refresh.
// At this point, the current queryReference will have been disposed
// by the previous cleanup, so instead of attempting to
// do our regular commit setup, which would incorrectly leave our
// current queryReference disposed, we need to load the query again
// and force a re-render by calling queryLoaderCallback again,
// so that the queryReference is correctly re-retained, and
// potentially refetched if necessary.
maybeHiddenOrFastRefresh.current = false;
if (queryReference.kind !== 'NullQueryReference') {
queryLoaderCallback(queryReference.variables, {
fetchPolicy: queryReference.fetchPolicy,
networkCacheConfig: queryReference.networkCacheConfig,
});
}
return;
}
// When a new queryReference is committed, we iterate over all
// query references in undisposedQueryReferences and dispose all of
// the refs that aren't the currently committed one. This ensures
// that we don't leave any dangling query references for the
// case that loadQuery is called multiple times before commit; when
// this happens, multiple state updates will be scheduled, but only one
// will commit, meaning that we need to keep track of and dispose any
// query references that don't end up committing.
// - We are relying on the fact that sets iterate in insertion order, and we
// can remove items from a set as we iterate over it (i.e. no iterator
// invalidation issues.) Thus, it is safe to loop through
// undisposedQueryReferences until we find queryReference, and
// remove and dispose all previous references.
// - We are guaranteed to find queryReference in the set, because if a
// state update results in a commit, no state updates initiated prior to that
// one will be committed, and we are disposing and removing references
// associated with updates that were scheduled prior to the currently
// committing state change. (A useEffect callback is called during the commit
// phase.)
const undisposedQueryReferences = undisposedQueryReferencesRef.current;
if (isMountedRef.current) {
for (const undisposedQueryReference of undisposedQueryReferences) {
if (undisposedQueryReference === queryReference) {
break;
}
undisposedQueryReferences.delete(undisposedQueryReference);
if (undisposedQueryReference.kind !== 'NullQueryReference') {
if (requestIsLiveQuery(preloadableRequest)) {
undisposedQueryReference.dispose &&
undisposedQueryReference.dispose();
} else {
undisposedQueryReference.releaseQuery &&
undisposedQueryReference.releaseQuery();
}
}
}
}
}, [queryReference, isMountedRef, queryLoaderCallback, preloadableRequest]);
useEffect(() => {
return function disposeAllRemainingQueryReferences() {
// undisposedQueryReferences.current is never reassigned
// eslint-disable-next-line react-hooks/exhaustive-deps
for (const undisposedQueryReference of undisposedQueryReferencesRef.current) {
if (undisposedQueryReference.kind !== 'NullQueryReference') {
if (requestIsLiveQuery(preloadableRequest)) {
undisposedQueryReference.dispose &&
undisposedQueryReference.dispose();
} else {
undisposedQueryReference.releaseQuery &&
undisposedQueryReference.releaseQuery();
}
}
}
};
}, [preloadableRequest]);
return [
queryReference.kind === 'NullQueryReference' ? null : queryReference,
queryLoaderCallback,
disposeQuery,
];
}
module.exports = useQueryLoader;