UNPKG

18.4 kBJavaScriptView Raw
1/**
2 * Copyright (c) Facebook, Inc. and its 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 * @emails oncall+relay
8 *
9 * @format
10 */
11'use strict';
12
13var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
14
15var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread"));
16
17var ProfilerContext = require('./ProfilerContext'); // flowlint untyped-import:off
18
19
20var Scheduler = require('scheduler'); // flowlint untyped-import:error
21
22
23var getRefetchMetadata = require('./getRefetchMetadata');
24
25var getValueAtPath = require('./getValueAtPath');
26
27var invariant = require("fbjs/lib/invariant");
28
29var useFetchTrackingRef = require('./useFetchTrackingRef');
30
31var useFragmentNode = require('./useFragmentNode');
32
33var useIsMountedRef = require('./useIsMountedRef');
34
35var useMemoVariables = require('./useMemoVariables');
36
37var useRelayEnvironment = require('./useRelayEnvironment');
38
39var warning = require("fbjs/lib/warning");
40
41var _require = require('./FragmentResource'),
42 getFragmentResourceForEnvironment = _require.getFragmentResourceForEnvironment;
43
44var _require2 = require('./QueryResource'),
45 getQueryResourceForEnvironment = _require2.getQueryResourceForEnvironment;
46
47var _require3 = require('react'),
48 useCallback = _require3.useCallback,
49 useContext = _require3.useContext,
50 useEffect = _require3.useEffect,
51 useMemo = _require3.useMemo,
52 useReducer = _require3.useReducer,
53 useRef = _require3.useRef;
54
55var _require4 = require('relay-runtime'),
56 fetchQuery = _require4.__internal.fetchQuery,
57 createOperationDescriptor = _require4.createOperationDescriptor,
58 getFragmentIdentifier = _require4.getFragmentIdentifier,
59 getSelector = _require4.getSelector;
60
61function reducer(state, action) {
62 switch (action.type) {
63 case 'refetch':
64 {
65 var _action$environment;
66
67 return (0, _objectSpread2["default"])({}, state, {
68 refetchVariables: action.refetchVariables,
69 fetchPolicy: action.fetchPolicy,
70 renderPolicy: action.renderPolicy,
71 onComplete: action.onComplete,
72 refetchEnvironment: action.environment,
73 mirroredEnvironment: (_action$environment = action.environment) !== null && _action$environment !== void 0 ? _action$environment : state.mirroredEnvironment
74 });
75 }
76
77 case 'reset':
78 {
79 return {
80 fetchPolicy: undefined,
81 renderPolicy: undefined,
82 onComplete: undefined,
83 refetchVariables: null,
84 mirroredEnvironment: action.environment,
85 mirroredFragmentIdentifier: action.fragmentIdentifier
86 };
87 }
88
89 default:
90 {
91 action.type;
92 throw new Error('useRefetchableFragmentNode: Unexpected action type');
93 }
94 }
95}
96
97function useRefetchableFragmentNode(fragmentNode, parentFragmentRef, componentDisplayName) {
98 var _refetchEnvironment;
99
100 var parentEnvironment = useRelayEnvironment();
101
102 var _getRefetchMetadata = getRefetchMetadata(fragmentNode, componentDisplayName),
103 refetchableRequest = _getRefetchMetadata.refetchableRequest,
104 fragmentRefPathInResponse = _getRefetchMetadata.fragmentRefPathInResponse;
105
106 var fragmentIdentifier = getFragmentIdentifier(fragmentNode, parentFragmentRef);
107
108 var _useReducer = useReducer(reducer, {
109 fetchPolicy: undefined,
110 renderPolicy: undefined,
111 onComplete: undefined,
112 refetchVariables: null,
113 refetchEnvironment: null,
114 mirroredEnvironment: parentEnvironment,
115 mirroredFragmentIdentifier: fragmentIdentifier
116 }),
117 refetchState = _useReducer[0],
118 dispatch = _useReducer[1];
119
120 var _useFetchTrackingRef = useFetchTrackingRef(),
121 startFetch = _useFetchTrackingRef.startFetch,
122 disposeFetch = _useFetchTrackingRef.disposeFetch,
123 completeFetch = _useFetchTrackingRef.completeFetch;
124
125 var refetchGenerationRef = useRef(0);
126 var refetchVariables = refetchState.refetchVariables,
127 refetchEnvironment = refetchState.refetchEnvironment,
128 fetchPolicy = refetchState.fetchPolicy,
129 renderPolicy = refetchState.renderPolicy,
130 onComplete = refetchState.onComplete,
131 mirroredEnvironment = refetchState.mirroredEnvironment,
132 mirroredFragmentIdentifier = refetchState.mirroredFragmentIdentifier;
133 var environment = (_refetchEnvironment = refetchEnvironment) !== null && _refetchEnvironment !== void 0 ? _refetchEnvironment : parentEnvironment;
134 var QueryResource = getQueryResourceForEnvironment(environment);
135 var profilerContext = useContext(ProfilerContext);
136 var shouldReset = environment !== mirroredEnvironment || fragmentIdentifier !== mirroredFragmentIdentifier;
137
138 var _useMemoVariables = useMemoVariables(refetchVariables),
139 memoRefetchVariables = _useMemoVariables[0];
140
141 var refetchQuery = useMemo(function () {
142 return memoRefetchVariables != null ? createOperationDescriptor(refetchableRequest, memoRefetchVariables) : null;
143 }, [memoRefetchVariables, refetchableRequest]);
144 var refetchedQueryResult;
145 var fragmentRef = parentFragmentRef;
146
147 if (shouldReset) {
148 dispatch({
149 type: 'reset',
150 environment: environment,
151 fragmentIdentifier: fragmentIdentifier
152 });
153 } else if (refetchQuery != null) {
154 var _refetchGenerationRef;
155
156 // check __typename/id is consistent if refetch existing data on Node
157 var debugPreviousIDAndTypename;
158
159 if (process.env.NODE_ENV !== "production") {
160 debugPreviousIDAndTypename = debugFunctions.getInitialIDAndType(memoRefetchVariables, fragmentRefPathInResponse, environment);
161 } // If refetch has been called, we read/fetch the refetch query here. If
162 // the refetch query hasn't been fetched or isn't cached, we will suspend
163 // at this point.
164
165
166 var _readQuery = readQuery(environment, refetchQuery, fetchPolicy, renderPolicy, (_refetchGenerationRef = refetchGenerationRef.current) !== null && _refetchGenerationRef !== void 0 ? _refetchGenerationRef : 0, componentDisplayName, {
167 start: startFetch,
168 complete: function complete(maybeError) {
169 var _maybeError;
170
171 completeFetch();
172 onComplete && onComplete((_maybeError = maybeError) !== null && _maybeError !== void 0 ? _maybeError : null);
173
174 if (process.env.NODE_ENV !== "production") {
175 if (!maybeError) {
176 debugFunctions.checkSameTypeAfterRefetch(debugPreviousIDAndTypename, environment, fragmentNode, componentDisplayName);
177 }
178 }
179 }
180 }, profilerContext),
181 queryResult = _readQuery[0],
182 queryData = _readQuery[1];
183
184 refetchedQueryResult = queryResult; // After reading/fetching the refetch query, we extract from the
185 // refetch query response the new fragment ref we need to use to read
186 // the fragment. The new fragment ref will point to the refetch query
187 // as its fragment owner.
188
189 var refetchedFragmentRef = getValueAtPath(queryData, fragmentRefPathInResponse);
190 fragmentRef = refetchedFragmentRef;
191
192 if (process.env.NODE_ENV !== "production") {
193 debugFunctions.checkSameIDAfterRefetch(debugPreviousIDAndTypename, fragmentRef, fragmentNode, componentDisplayName);
194 }
195 } // We read and subscribe to the fragment using useFragmentNode.
196 // If refetch was called, we read the fragment using the new computed
197 // fragment ref from the refetch query response; otherwise, we use the
198 // fragment ref passed by the caller as normal.
199
200
201 var _useFragmentNode = useFragmentNode(fragmentNode, fragmentRef, componentDisplayName),
202 fragmentData = _useFragmentNode.data,
203 disableStoreUpdates = _useFragmentNode.disableStoreUpdates,
204 enableStoreUpdates = _useFragmentNode.enableStoreUpdates;
205
206 useEffect(function () {
207 // Retain the refetch query if it was fetched and release it
208 // in the useEffect cleanup.
209 var queryDisposable = refetchedQueryResult != null ? QueryResource.retain(refetchedQueryResult) : null;
210 return function () {
211 if (queryDisposable) {
212 queryDisposable.dispose();
213 }
214 }; // NOTE: We disable react-hooks-deps warning because
215 // refetchedQueryResult is captured by including refetchQuery, which is
216 // already capturing if the query or variables changed.
217 // eslint-disable-next-line react-hooks/exhaustive-deps
218 }, [QueryResource, fragmentIdentifier, refetchQuery]);
219 var refetch = useRefetchFunction(fragmentNode, parentFragmentRef, fragmentIdentifier, fragmentRefPathInResponse, fragmentData, refetchGenerationRef, dispatch, disposeFetch, componentDisplayName);
220 return {
221 fragmentData: fragmentData,
222 fragmentRef: fragmentRef,
223 refetch: refetch,
224 disableStoreUpdates: disableStoreUpdates,
225 enableStoreUpdates: enableStoreUpdates
226 };
227}
228
229function useRefetchFunction(fragmentNode, parentFragmentRef, fragmentIdentifier, fragmentRefPathInResponse, fragmentData, refetchGenerationRef, dispatch, disposeFetch, componentDisplayName) {
230 var isMountedRef = useIsMountedRef(); // $FlowFixMe
231
232 var dataID = fragmentData === null || fragmentData === void 0 ? void 0 : fragmentData.id;
233 return useCallback(function (providedRefetchVariables, options) {
234 var _refetchGenerationRef2;
235
236 // Bail out and warn if we're trying to refetch after the component
237 // has unmounted
238 if (isMountedRef.current !== true) {
239 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Unexpected call to `refetch` on unmounted component for fragment ' + '`%s` in `%s`. It looks like some instances of your component are ' + 'still trying to fetch data but they already unmounted. ' + 'Please make sure you clear all timers, intervals, ' + 'async calls, etc that may trigger a fetch.', fragmentNode.name, componentDisplayName) : void 0;
240 return {
241 dispose: function dispose() {}
242 };
243 }
244
245 if (Scheduler.unstable_getCurrentPriorityLevel() < Scheduler.unstable_NormalPriority) {
246 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Unexpected call to `refetch` at a priority higher than ' + 'expected on fragment `%s` in `%s`. It looks like you tried to ' + 'call `refetch` under a high priority update, but updates that ' + 'can cause the component to suspend should be scheduled at ' + 'normal priority. Make sure you are calling `refetch` inside ' + '`startTransition()` from the `useSuspenseTransition()` hook.', fragmentNode.name, componentDisplayName) : void 0;
247 }
248
249 if (parentFragmentRef == null) {
250 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Unexpected call to `refetch` while using a null fragment ref ' + 'for fragment `%s` in `%s`. When calling `refetch`, we expect ' + "initial fragment data to be non-null. Please make sure you're " + 'passing a valid fragment ref to `%s` before calling ' + '`refetch`, or make sure you pass all required variables to `refetch`.', fragmentNode.name, componentDisplayName, componentDisplayName) : void 0;
251 }
252
253 refetchGenerationRef.current = ((_refetchGenerationRef2 = refetchGenerationRef.current) !== null && _refetchGenerationRef2 !== void 0 ? _refetchGenerationRef2 : 0) + 1;
254 var environment = options === null || options === void 0 ? void 0 : options.__environment;
255 var fetchPolicy = options === null || options === void 0 ? void 0 : options.fetchPolicy;
256 var renderPolicy = options === null || options === void 0 ? void 0 : options.renderPolicy_UNSTABLE;
257 var onComplete = options === null || options === void 0 ? void 0 : options.onComplete;
258 var fragmentSelector = getSelector(fragmentNode, parentFragmentRef);
259 var parentVariables;
260 var fragmentVariables;
261
262 if (fragmentSelector == null) {
263 parentVariables = {};
264 fragmentVariables = {};
265 } else if (fragmentSelector.kind === 'PluralReaderSelector') {
266 var _ref, _fragmentSelector$sel, _ref2, _fragmentSelector$sel2;
267
268 parentVariables = (_ref = (_fragmentSelector$sel = fragmentSelector.selectors[0]) === null || _fragmentSelector$sel === void 0 ? void 0 : _fragmentSelector$sel.owner.variables) !== null && _ref !== void 0 ? _ref : {};
269 fragmentVariables = (_ref2 = (_fragmentSelector$sel2 = fragmentSelector.selectors[0]) === null || _fragmentSelector$sel2 === void 0 ? void 0 : _fragmentSelector$sel2.variables) !== null && _ref2 !== void 0 ? _ref2 : {};
270 } else {
271 parentVariables = fragmentSelector.owner.variables;
272 fragmentVariables = fragmentSelector.variables;
273 } // NOTE: A user of `useRefetchableFragment()` may pass a subset of
274 // all variables required by the fragment when calling `refetch()`.
275 // We fill in any variables not passed by the call to `refetch()` with the
276 // variables from the original parent fragment owner.
277
278
279 var refetchVariables = (0, _objectSpread2["default"])({}, parentVariables, fragmentVariables, providedRefetchVariables); // TODO (T40777961): Tweak output of @refetchable transform to more
280 // easily tell if we need an $id in the refetch vars
281
282 if (fragmentRefPathInResponse.includes('node') && !providedRefetchVariables.hasOwnProperty('id')) {
283 // @refetchable fragments are guaranteed to have an `id` selection
284 // if the type is Node or implements Node. Double-check that there
285 // actually is a value at runtime.
286 if (typeof dataID !== 'string') {
287 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Expected result to have a string ' + '`id` in order to refetch, got `%s`.', dataID) : void 0;
288 }
289
290 refetchVariables.id = dataID;
291 }
292
293 dispatch({
294 type: 'refetch',
295 refetchVariables: refetchVariables,
296 fetchPolicy: fetchPolicy,
297 renderPolicy: renderPolicy,
298 onComplete: onComplete,
299 environment: environment
300 });
301 return {
302 dispose: disposeFetch
303 };
304 }, // NOTE: We disable react-hooks-deps warning because:
305 // - We know fragmentRefPathInResponse is static, so it can be omitted from
306 // deps
307 // - We know fragmentNode is static, so it can be omitted from deps.
308 // - fragmentNode and parentFragmentRef are also captured by including
309 // fragmentIdentifier
310 // eslint-disable-next-line react-hooks/exhaustive-deps
311 [fragmentIdentifier, dataID, dispatch, disposeFetch]);
312}
313
314function readQuery(environment, query, fetchPolicy, renderPolicy, refetchGeneration, componentDisplayName, _ref3, profilerContext) {
315 var start = _ref3.start,
316 complete = _ref3.complete;
317 var QueryResource = getQueryResourceForEnvironment(environment);
318 var FragmentResource = getFragmentResourceForEnvironment(environment);
319 var queryResult = profilerContext.wrapPrepareQueryResource(function () {
320 return QueryResource.prepare(query, fetchQuery(environment, query, {
321 networkCacheConfig: {
322 force: true
323 }
324 }), fetchPolicy, renderPolicy, {
325 start: start,
326 error: complete,
327 complete: complete
328 }, // NOTE: QueryResource will keep a cache entry for a query for the
329 // entire lifetime of this component. However, every time refetch is
330 // called, we want to make sure we correctly attempt to fetch the query
331 // (taking into account the fetchPolicy), even if we're refetching the exact
332 // same query (e.g. refreshing it).
333 // To do so, we keep track of every time refetch is called with
334 // `refetchGenerationRef`, which we can use as a key for the query in
335 // QueryResource.
336 refetchGeneration);
337 });
338 var queryData = FragmentResource.read(queryResult.fragmentNode, queryResult.fragmentRef, componentDisplayName).data;
339 !(queryData != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Relay: Expected to be able to read refetch query response. ' + "If you're seeing this, this is likely a bug in Relay.") : invariant(false) : void 0;
340 return [queryResult, queryData];
341}
342
343var debugFunctions;
344
345if (process.env.NODE_ENV !== "production") {
346 debugFunctions = {
347 getInitialIDAndType: function getInitialIDAndType(memoRefetchVariables, fragmentRefPathInResponse, environment) {
348 var _require5 = require('relay-runtime'),
349 Record = _require5.Record;
350
351 var id = memoRefetchVariables === null || memoRefetchVariables === void 0 ? void 0 : memoRefetchVariables.id;
352
353 if (fragmentRefPathInResponse.length !== 1 || fragmentRefPathInResponse[0] !== 'node' || id == null) {
354 return null;
355 }
356
357 var recordSource = environment.getStore().getSource();
358 var record = recordSource.get(id);
359 var typename = record && Record.getType(record);
360
361 if (typename == null) {
362 return null;
363 }
364
365 return {
366 id: id,
367 typename: typename
368 };
369 },
370 checkSameTypeAfterRefetch: function checkSameTypeAfterRefetch(previousIDAndType, environment, fragmentNode, componentDisplayName) {
371 var _require6 = require('relay-runtime'),
372 Record = _require6.Record;
373
374 if (!previousIDAndType) {
375 return;
376 }
377
378 var recordSource = environment.getStore().getSource();
379 var record = recordSource.get(previousIDAndType.id);
380 var typename = record && Record.getType(record);
381
382 if (typename !== previousIDAndType.typename) {
383 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Call to `refetch` returned data with a different ' + '__typename: was `%s`, now `%s`, on `%s` in `%s`. ' + 'Please make sure the server correctly implements' + 'unique id requirement.', previousIDAndType.typename, typename, fragmentNode.name, componentDisplayName) : void 0;
384 }
385 },
386 checkSameIDAfterRefetch: function checkSameIDAfterRefetch(previousIDAndTypename, refetchedFragmentRef, fragmentNode, componentDisplayName) {
387 if (previousIDAndTypename == null) {
388 return;
389 }
390
391 var _require7 = require('relay-runtime'),
392 ID_KEY = _require7.ID_KEY; // $FlowExpectedError
393
394
395 var resultID = refetchedFragmentRef[ID_KEY];
396
397 if (resultID != null && resultID !== previousIDAndTypename.id) {
398 process.env.NODE_ENV !== "production" ? warning(false, 'Relay: Call to `refetch` returned a different id, expected ' + '`%s`, got `%s`, on `%s` in `%s`. ' + 'Please make sure the server correctly implements ' + 'unique id requirement.', resultID, previousIDAndTypename.id, fragmentNode.name, componentDisplayName) : void 0;
399 }
400 }
401 };
402}
403
404module.exports = useRefetchableFragmentNode;
\No newline at end of file