UNPKG

23.7 kBJavaScriptView Raw
1import { __assign, __rest } from "tslib";
2/**
3 * Function parameters in this file try to follow a common order for the sake of
4 * readability and consistency. The order is as follows:
5 *
6 * resultData
7 * observable
8 * client
9 * query
10 * options
11 * watchQueryOptions
12 * makeWatchQueryOptions
13 * isSSRAllowed
14 * disableNetworkFetches
15 * partialRefetch
16 * renderPromises
17 * isSyncSSR
18 * callbacks
19 */
20/** */
21import { invariant } from "../../utilities/globals/index.js";
22import * as React from "rehackt";
23import { useSyncExternalStore } from "./useSyncExternalStore.js";
24import { equal } from "@wry/equality";
25import { mergeOptions } from "../../utilities/index.js";
26import { getApolloContext } from "../context/index.js";
27import { ApolloError } from "../../errors/index.js";
28import { NetworkStatus } from "../../core/index.js";
29import { DocumentType, verifyDocumentType } from "../parser/index.js";
30import { useApolloClient } from "./useApolloClient.js";
31import { compact, isNonEmptyArray, maybeDeepFreeze, } from "../../utilities/index.js";
32import { wrapHook } from "./internal/index.js";
33var hasOwnProperty = Object.prototype.hasOwnProperty;
34function noop() { }
35export var lastWatchOptions = Symbol();
36/**
37 * A hook for executing queries in an Apollo application.
38 *
39 * To run a query within a React component, call `useQuery` and pass it a GraphQL query document.
40 *
41 * When your component renders, `useQuery` returns an object from Apollo Client that contains `loading`, `error`, and `data` properties you can use to render your UI.
42 *
43 * > Refer to the [Queries](https://www.apollographql.com/docs/react/data/queries) section for a more in-depth overview of `useQuery`.
44 *
45 * @example
46 * ```jsx
47 * import { gql, useQuery } from '@apollo/client';
48 *
49 * const GET_GREETING = gql`
50 * query GetGreeting($language: String!) {
51 * greeting(language: $language) {
52 * message
53 * }
54 * }
55 * `;
56 *
57 * function Hello() {
58 * const { loading, error, data } = useQuery(GET_GREETING, {
59 * variables: { language: 'english' },
60 * });
61 * if (loading) return <p>Loading ...</p>;
62 * return <h1>Hello {data.greeting.message}!</h1>;
63 * }
64 * ```
65 * @since 3.0.0
66 * @param query - A GraphQL query document parsed into an AST by `gql`.
67 * @param options - Options to control how the query is executed.
68 * @returns Query result object
69 */
70export function useQuery(query, options) {
71 if (options === void 0) { options = Object.create(null); }
72 return wrapHook("useQuery", _useQuery, useApolloClient(options && options.client))(query, options);
73}
74function _useQuery(query, options) {
75 var _a = useQueryInternals(query, options), result = _a.result, obsQueryFields = _a.obsQueryFields;
76 return React.useMemo(function () { return (__assign(__assign({}, result), obsQueryFields)); }, [result, obsQueryFields]);
77}
78function useInternalState(client, query, options, renderPromises, makeWatchQueryOptions) {
79 function createInternalState(previous) {
80 var _a;
81 verifyDocumentType(query, DocumentType.Query);
82 var internalState = {
83 client: client,
84 query: query,
85 observable:
86 // See if there is an existing observable that was used to fetch the same
87 // data and if so, use it instead since it will contain the proper queryId
88 // to fetch the result set. This is used during SSR.
89 (renderPromises &&
90 renderPromises.getSSRObservable(makeWatchQueryOptions())) ||
91 client.watchQuery(getObsQueryOptions(void 0, client, options, makeWatchQueryOptions())),
92 resultData: {
93 // Reuse previousData from previous InternalState (if any) to provide
94 // continuity of previousData even if/when the query or client changes.
95 previousData: (_a = previous === null || previous === void 0 ? void 0 : previous.resultData.current) === null || _a === void 0 ? void 0 : _a.data,
96 },
97 };
98 return internalState;
99 }
100 var _a = React.useState(createInternalState), internalState = _a[0], updateInternalState = _a[1];
101 /**
102 * Used by `useLazyQuery` when a new query is executed.
103 * We keep this logic here since it needs to update things in unsafe
104 * ways and here we at least can keep track of that in a single place.
105 */
106 function onQueryExecuted(watchQueryOptions) {
107 var _a;
108 var _b;
109 // this needs to be set to prevent an immediate `resubscribe` in the
110 // next rerender of the `useQuery` internals
111 Object.assign(internalState.observable, (_a = {},
112 _a[lastWatchOptions] = watchQueryOptions,
113 _a));
114 var resultData = internalState.resultData;
115 updateInternalState(__assign(__assign({}, internalState), {
116 // might be a different query
117 query: watchQueryOptions.query, resultData: Object.assign(resultData, {
118 // We need to modify the previous `resultData` object as we rely on the
119 // object reference in other places
120 previousData: ((_b = resultData.current) === null || _b === void 0 ? void 0 : _b.data) || resultData.previousData,
121 current: undefined,
122 }) }));
123 }
124 if (client !== internalState.client || query !== internalState.query) {
125 // If the client or query have changed, we need to create a new InternalState.
126 // This will trigger a re-render with the new state, but it will also continue
127 // to run the current render function to completion.
128 // Since we sometimes trigger some side-effects in the render function, we
129 // re-assign `state` to the new state to ensure that those side-effects are
130 // triggered with the new state.
131 var newInternalState = createInternalState(internalState);
132 updateInternalState(newInternalState);
133 return [newInternalState, onQueryExecuted];
134 }
135 return [internalState, onQueryExecuted];
136}
137export function useQueryInternals(query, options) {
138 var client = useApolloClient(options.client);
139 var renderPromises = React.useContext(getApolloContext()).renderPromises;
140 var isSyncSSR = !!renderPromises;
141 var disableNetworkFetches = client.disableNetworkFetches;
142 var ssrAllowed = options.ssr !== false && !options.skip;
143 var partialRefetch = options.partialRefetch;
144 var makeWatchQueryOptions = createMakeWatchQueryOptions(client, query, options, isSyncSSR);
145 var _a = useInternalState(client, query, options, renderPromises, makeWatchQueryOptions), _b = _a[0], observable = _b.observable, resultData = _b.resultData, onQueryExecuted = _a[1];
146 var watchQueryOptions = makeWatchQueryOptions(observable);
147 useResubscribeIfNecessary(resultData, // might get mutated during render
148 observable, // might get mutated during render
149 client, options, watchQueryOptions);
150 var obsQueryFields = React.useMemo(function () { return bindObservableMethods(observable); }, [observable]);
151 useRegisterSSRObservable(observable, renderPromises, ssrAllowed);
152 var result = useObservableSubscriptionResult(resultData, observable, client, options, watchQueryOptions, disableNetworkFetches, partialRefetch, isSyncSSR, {
153 onCompleted: options.onCompleted || noop,
154 onError: options.onError || noop,
155 });
156 return {
157 result: result,
158 obsQueryFields: obsQueryFields,
159 observable: observable,
160 resultData: resultData,
161 client: client,
162 onQueryExecuted: onQueryExecuted,
163 };
164}
165function useObservableSubscriptionResult(resultData, observable, client, options, watchQueryOptions, disableNetworkFetches, partialRefetch, isSyncSSR, callbacks) {
166 var callbackRef = React.useRef(callbacks);
167 React.useEffect(function () {
168 // Make sure state.onCompleted and state.onError always reflect the latest
169 // options.onCompleted and options.onError callbacks provided to useQuery,
170 // since those functions are often recreated every time useQuery is called.
171 // Like the forceUpdate method, the versions of these methods inherited from
172 // InternalState.prototype are empty no-ops, but we can override them on the
173 // base state object (without modifying the prototype).
174 callbackRef.current = callbacks;
175 });
176 var resultOverride = ((isSyncSSR || disableNetworkFetches) &&
177 options.ssr === false &&
178 !options.skip) ?
179 // If SSR has been explicitly disabled, and this function has been called
180 // on the server side, return the default loading state.
181 ssrDisabledResult
182 : options.skip || watchQueryOptions.fetchPolicy === "standby" ?
183 // When skipping a query (ie. we're not querying for data but still want to
184 // render children), make sure the `data` is cleared out and `loading` is
185 // set to `false` (since we aren't loading anything).
186 //
187 // NOTE: We no longer think this is the correct behavior. Skipping should
188 // not automatically set `data` to `undefined`, but instead leave the
189 // previous data in place. In other words, skipping should not mandate that
190 // previously received data is all of a sudden removed. Unfortunately,
191 // changing this is breaking, so we'll have to wait until Apollo Client 4.0
192 // to address this.
193 skipStandbyResult
194 : void 0;
195 var previousData = resultData.previousData;
196 var currentResultOverride = React.useMemo(function () {
197 return resultOverride &&
198 toQueryResult(resultOverride, previousData, observable, client);
199 }, [client, observable, resultOverride, previousData]);
200 return useSyncExternalStore(React.useCallback(function (handleStoreChange) {
201 // reference `disableNetworkFetches` here to ensure that the rules of hooks
202 // keep it as a dependency of this effect, even though it's not used
203 disableNetworkFetches;
204 if (isSyncSSR) {
205 return function () { };
206 }
207 var onNext = function () {
208 var previousResult = resultData.current;
209 // We use `getCurrentResult()` instead of the onNext argument because
210 // the values differ slightly. Specifically, loading results will have
211 // an empty object for data instead of `undefined` for some reason.
212 var result = observable.getCurrentResult();
213 // Make sure we're not attempting to re-render similar results
214 if (previousResult &&
215 previousResult.loading === result.loading &&
216 previousResult.networkStatus === result.networkStatus &&
217 equal(previousResult.data, result.data)) {
218 return;
219 }
220 setResult(result, resultData, observable, client, partialRefetch, handleStoreChange, callbackRef.current);
221 };
222 var onError = function (error) {
223 subscription.current.unsubscribe();
224 subscription.current = observable.resubscribeAfterError(onNext, onError);
225 if (!hasOwnProperty.call(error, "graphQLErrors")) {
226 // The error is not a GraphQL error
227 throw error;
228 }
229 var previousResult = resultData.current;
230 if (!previousResult ||
231 (previousResult && previousResult.loading) ||
232 !equal(error, previousResult.error)) {
233 setResult({
234 data: (previousResult && previousResult.data),
235 error: error,
236 loading: false,
237 networkStatus: NetworkStatus.error,
238 }, resultData, observable, client, partialRefetch, handleStoreChange, callbackRef.current);
239 }
240 };
241 // TODO evaluate if we keep this in
242 // React Compiler cannot handle scoped `let` access, but a mutable object
243 // like this is fine.
244 // was:
245 // let subscription = observable.subscribe(onNext, onError);
246 var subscription = { current: observable.subscribe(onNext, onError) };
247 // Do the "unsubscribe" with a short delay.
248 // This way, an existing subscription can be reused without an additional
249 // request if "unsubscribe" and "resubscribe" to the same ObservableQuery
250 // happen in very fast succession.
251 return function () {
252 setTimeout(function () { return subscription.current.unsubscribe(); });
253 };
254 }, [
255 disableNetworkFetches,
256 isSyncSSR,
257 observable,
258 resultData,
259 partialRefetch,
260 client,
261 ]), function () {
262 return currentResultOverride ||
263 getCurrentResult(resultData, observable, callbackRef.current, partialRefetch, client);
264 }, function () {
265 return currentResultOverride ||
266 getCurrentResult(resultData, observable, callbackRef.current, partialRefetch, client);
267 });
268}
269function useRegisterSSRObservable(observable, renderPromises, ssrAllowed) {
270 if (renderPromises && ssrAllowed) {
271 renderPromises.registerSSRObservable(observable);
272 if (observable.getCurrentResult().loading) {
273 // TODO: This is a legacy API which could probably be cleaned up
274 renderPromises.addObservableQueryPromise(observable);
275 }
276 }
277}
278// this hook is not compatible with any rules of React, and there's no good way to rewrite it.
279// it should stay a separate hook that will not be optimized by the compiler
280function useResubscribeIfNecessary(
281/** this hook will mutate properties on `resultData` */
282resultData,
283/** this hook will mutate properties on `observable` */
284observable, client, options, watchQueryOptions) {
285 var _a;
286 if (observable[lastWatchOptions] &&
287 !equal(observable[lastWatchOptions], watchQueryOptions)) {
288 // Though it might be tempting to postpone this reobserve call to the
289 // useEffect block, we need getCurrentResult to return an appropriate
290 // loading:true result synchronously (later within the same call to
291 // useQuery). Since we already have this.observable here (not true for
292 // the very first call to useQuery), we are not initiating any new
293 // subscriptions, though it does feel less than ideal that reobserve
294 // (potentially) kicks off a network request (for example, when the
295 // variables have changed), which is technically a side-effect.
296 observable.reobserve(getObsQueryOptions(observable, client, options, watchQueryOptions));
297 // Make sure getCurrentResult returns a fresh ApolloQueryResult<TData>,
298 // but save the current data as this.previousData, just like setResult
299 // usually does.
300 resultData.previousData =
301 ((_a = resultData.current) === null || _a === void 0 ? void 0 : _a.data) || resultData.previousData;
302 resultData.current = void 0;
303 }
304 observable[lastWatchOptions] = watchQueryOptions;
305}
306/*
307 * A function to massage options before passing them to ObservableQuery.
308 * This is two-step curried because we want to reuse the `make` function,
309 * but the `observable` might differ between calls to `make`.
310 */
311export function createMakeWatchQueryOptions(client, query, _a, isSyncSSR) {
312 if (_a === void 0) { _a = {}; }
313 var skip = _a.skip, ssr = _a.ssr, onCompleted = _a.onCompleted, onError = _a.onError, defaultOptions = _a.defaultOptions,
314 // The above options are useQuery-specific, so this ...otherOptions spread
315 // makes otherOptions almost a WatchQueryOptions object, except for the
316 // query property that we add below.
317 otherOptions = __rest(_a, ["skip", "ssr", "onCompleted", "onError", "defaultOptions"]);
318 return function (observable) {
319 // This Object.assign is safe because otherOptions is a fresh ...rest object
320 // that did not exist until just now, so modifications are still allowed.
321 var watchQueryOptions = Object.assign(otherOptions, { query: query });
322 if (isSyncSSR &&
323 (watchQueryOptions.fetchPolicy === "network-only" ||
324 watchQueryOptions.fetchPolicy === "cache-and-network")) {
325 // this behavior was added to react-apollo without explanation in this PR
326 // https://github.com/apollographql/react-apollo/pull/1579
327 watchQueryOptions.fetchPolicy = "cache-first";
328 }
329 if (!watchQueryOptions.variables) {
330 watchQueryOptions.variables = {};
331 }
332 if (skip) {
333 // When skipping, we set watchQueryOptions.fetchPolicy initially to
334 // "standby", but we also need/want to preserve the initial non-standby
335 // fetchPolicy that would have been used if not skipping.
336 watchQueryOptions.initialFetchPolicy =
337 watchQueryOptions.initialFetchPolicy ||
338 watchQueryOptions.fetchPolicy ||
339 getDefaultFetchPolicy(defaultOptions, client.defaultOptions);
340 watchQueryOptions.fetchPolicy = "standby";
341 }
342 else if (!watchQueryOptions.fetchPolicy) {
343 watchQueryOptions.fetchPolicy =
344 (observable === null || observable === void 0 ? void 0 : observable.options.initialFetchPolicy) ||
345 getDefaultFetchPolicy(defaultOptions, client.defaultOptions);
346 }
347 return watchQueryOptions;
348 };
349}
350export function getObsQueryOptions(observable, client, queryHookOptions, watchQueryOptions) {
351 var toMerge = [];
352 var globalDefaults = client.defaultOptions.watchQuery;
353 if (globalDefaults)
354 toMerge.push(globalDefaults);
355 if (queryHookOptions.defaultOptions) {
356 toMerge.push(queryHookOptions.defaultOptions);
357 }
358 // We use compact rather than mergeOptions for this part of the merge,
359 // because we want watchQueryOptions.variables (if defined) to replace
360 // this.observable.options.variables whole. This replacement allows
361 // removing variables by removing them from the variables input to
362 // useQuery. If the variables were always merged together (rather than
363 // replaced), there would be no way to remove existing variables.
364 // However, the variables from options.defaultOptions and globalDefaults
365 // (if provided) should be merged, to ensure individual defaulted
366 // variables always have values, if not otherwise defined in
367 // observable.options or watchQueryOptions.
368 toMerge.push(compact(observable && observable.options, watchQueryOptions));
369 return toMerge.reduce(mergeOptions);
370}
371function setResult(nextResult, resultData, observable, client, partialRefetch, forceUpdate, callbacks) {
372 var previousResult = resultData.current;
373 if (previousResult && previousResult.data) {
374 resultData.previousData = previousResult.data;
375 }
376 if (!nextResult.error && isNonEmptyArray(nextResult.errors)) {
377 // Until a set naming convention for networkError and graphQLErrors is
378 // decided upon, we map errors (graphQLErrors) to the error options.
379 // TODO: Is it possible for both result.error and result.errors to be
380 // defined here?
381 nextResult.error = new ApolloError({ graphQLErrors: nextResult.errors });
382 }
383 resultData.current = toQueryResult(unsafeHandlePartialRefetch(nextResult, observable, partialRefetch), resultData.previousData, observable, client);
384 // Calling state.setResult always triggers an update, though some call sites
385 // perform additional equality checks before committing to an update.
386 forceUpdate();
387 handleErrorOrCompleted(nextResult, previousResult === null || previousResult === void 0 ? void 0 : previousResult.networkStatus, callbacks);
388}
389function handleErrorOrCompleted(result, previousNetworkStatus, callbacks) {
390 if (!result.loading) {
391 var error_1 = toApolloError(result);
392 // wait a tick in case we are in the middle of rendering a component
393 Promise.resolve()
394 .then(function () {
395 if (error_1) {
396 callbacks.onError(error_1);
397 }
398 else if (result.data &&
399 previousNetworkStatus !== result.networkStatus &&
400 result.networkStatus === NetworkStatus.ready) {
401 callbacks.onCompleted(result.data);
402 }
403 })
404 .catch(function (error) {
405 globalThis.__DEV__ !== false && invariant.warn(error);
406 });
407 }
408}
409function getCurrentResult(resultData, observable, callbacks, partialRefetch, client) {
410 // Using this.result as a cache ensures getCurrentResult continues returning
411 // the same (===) result object, unless state.setResult has been called, or
412 // we're doing server rendering and therefore override the result below.
413 if (!resultData.current) {
414 // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION
415 // this could call unsafeHandlePartialRefetch
416 setResult(observable.getCurrentResult(), resultData, observable, client, partialRefetch, function () { }, callbacks);
417 }
418 return resultData.current;
419}
420export function getDefaultFetchPolicy(queryHookDefaultOptions, clientDefaultOptions) {
421 var _a;
422 return ((queryHookDefaultOptions === null || queryHookDefaultOptions === void 0 ? void 0 : queryHookDefaultOptions.fetchPolicy) ||
423 ((_a = clientDefaultOptions === null || clientDefaultOptions === void 0 ? void 0 : clientDefaultOptions.watchQuery) === null || _a === void 0 ? void 0 : _a.fetchPolicy) ||
424 "cache-first");
425}
426export function toApolloError(result) {
427 return isNonEmptyArray(result.errors) ?
428 new ApolloError({ graphQLErrors: result.errors })
429 : result.error;
430}
431export function toQueryResult(result, previousData, observable, client) {
432 var data = result.data, partial = result.partial, resultWithoutPartial = __rest(result, ["data", "partial"]);
433 var queryResult = __assign(__assign({ data: data }, resultWithoutPartial), { client: client, observable: observable, variables: observable.variables, called: result !== ssrDisabledResult && result !== skipStandbyResult, previousData: previousData });
434 return queryResult;
435}
436function unsafeHandlePartialRefetch(result, observable, partialRefetch) {
437 // TODO: This code should be removed when the partialRefetch option is
438 // removed. I was unable to get this hook to behave reasonably in certain
439 // edge cases when this block was put in an effect.
440 if (result.partial &&
441 partialRefetch &&
442 !result.loading &&
443 (!result.data || Object.keys(result.data).length === 0) &&
444 observable.options.fetchPolicy !== "cache-only") {
445 observable.refetch();
446 return __assign(__assign({}, result), { loading: true, networkStatus: NetworkStatus.refetch });
447 }
448 return result;
449}
450var ssrDisabledResult = maybeDeepFreeze({
451 loading: true,
452 data: void 0,
453 error: void 0,
454 networkStatus: NetworkStatus.loading,
455});
456var skipStandbyResult = maybeDeepFreeze({
457 loading: false,
458 data: void 0,
459 error: void 0,
460 networkStatus: NetworkStatus.ready,
461});
462function bindObservableMethods(observable) {
463 return {
464 refetch: observable.refetch.bind(observable),
465 reobserve: observable.reobserve.bind(observable),
466 fetchMore: observable.fetchMore.bind(observable),
467 updateQuery: observable.updateQuery.bind(observable),
468 startPolling: observable.startPolling.bind(observable),
469 stopPolling: observable.stopPolling.bind(observable),
470 subscribeToMore: observable.subscribeToMore.bind(observable),
471 };
472}
473//# sourceMappingURL=useQuery.js.map
\No newline at end of file