UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

473 lines 23.7 kB
import { __assign, __rest } from "tslib"; /** * Function parameters in this file try to follow a common order for the sake of * readability and consistency. The order is as follows: * * resultData * observable * client * query * options * watchQueryOptions * makeWatchQueryOptions * isSSRAllowed * disableNetworkFetches * partialRefetch * renderPromises * isSyncSSR * callbacks */ /** */ import { invariant } from "../../utilities/globals/index.js"; import * as React from "rehackt"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; import { mergeOptions } from "../../utilities/index.js"; import { getApolloContext } from "../context/index.js"; import { ApolloError } from "../../errors/index.js"; import { NetworkStatus } from "../../core/index.js"; import { DocumentType, verifyDocumentType } from "../parser/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { compact, isNonEmptyArray, maybeDeepFreeze, } from "../../utilities/index.js"; import { wrapHook } from "./internal/index.js"; var hasOwnProperty = Object.prototype.hasOwnProperty; function noop() { } export var lastWatchOptions = Symbol(); /** * A hook for executing queries in an Apollo application. * * To run a query within a React component, call `useQuery` and pass it a GraphQL query document. * * 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. * * > Refer to the [Queries](https://www.apollographql.com/docs/react/data/queries) section for a more in-depth overview of `useQuery`. * * @example * ```jsx * import { gql, useQuery } from '@apollo/client'; * * const GET_GREETING = gql` * query GetGreeting($language: String!) { * greeting(language: $language) { * message * } * } * `; * * function Hello() { * const { loading, error, data } = useQuery(GET_GREETING, { * variables: { language: 'english' }, * }); * if (loading) return <p>Loading ...</p>; * return <h1>Hello {data.greeting.message}!</h1>; * } * ``` * @since 3.0.0 * @param query - A GraphQL query document parsed into an AST by `gql`. * @param options - Options to control how the query is executed. * @returns Query result object */ export function useQuery(query, options) { if (options === void 0) { options = Object.create(null); } return wrapHook("useQuery", _useQuery, useApolloClient(options && options.client))(query, options); } function _useQuery(query, options) { var _a = useQueryInternals(query, options), result = _a.result, obsQueryFields = _a.obsQueryFields; return React.useMemo(function () { return (__assign(__assign({}, result), obsQueryFields)); }, [result, obsQueryFields]); } function useInternalState(client, query, options, renderPromises, makeWatchQueryOptions) { function createInternalState(previous) { var _a; verifyDocumentType(query, DocumentType.Query); var internalState = { client: client, query: query, observable: // See if there is an existing observable that was used to fetch the same // data and if so, use it instead since it will contain the proper queryId // to fetch the result set. This is used during SSR. (renderPromises && renderPromises.getSSRObservable(makeWatchQueryOptions())) || client.watchQuery(getObsQueryOptions(void 0, client, options, makeWatchQueryOptions())), resultData: { // Reuse previousData from previous InternalState (if any) to provide // continuity of previousData even if/when the query or client changes. previousData: (_a = previous === null || previous === void 0 ? void 0 : previous.resultData.current) === null || _a === void 0 ? void 0 : _a.data, }, }; return internalState; } var _a = React.useState(createInternalState), internalState = _a[0], updateInternalState = _a[1]; /** * Used by `useLazyQuery` when a new query is executed. * We keep this logic here since it needs to update things in unsafe * ways and here we at least can keep track of that in a single place. */ function onQueryExecuted(watchQueryOptions) { var _a; var _b; // this needs to be set to prevent an immediate `resubscribe` in the // next rerender of the `useQuery` internals Object.assign(internalState.observable, (_a = {}, _a[lastWatchOptions] = watchQueryOptions, _a)); var resultData = internalState.resultData; updateInternalState(__assign(__assign({}, internalState), { // might be a different query query: watchQueryOptions.query, resultData: Object.assign(resultData, { // We need to modify the previous `resultData` object as we rely on the // object reference in other places previousData: ((_b = resultData.current) === null || _b === void 0 ? void 0 : _b.data) || resultData.previousData, current: undefined, }) })); } if (client !== internalState.client || query !== internalState.query) { // If the client or query have changed, we need to create a new InternalState. // This will trigger a re-render with the new state, but it will also continue // to run the current render function to completion. // Since we sometimes trigger some side-effects in the render function, we // re-assign `state` to the new state to ensure that those side-effects are // triggered with the new state. var newInternalState = createInternalState(internalState); updateInternalState(newInternalState); return [newInternalState, onQueryExecuted]; } return [internalState, onQueryExecuted]; } export function useQueryInternals(query, options) { var client = useApolloClient(options.client); var renderPromises = React.useContext(getApolloContext()).renderPromises; var isSyncSSR = !!renderPromises; var disableNetworkFetches = client.disableNetworkFetches; var ssrAllowed = options.ssr !== false && !options.skip; var partialRefetch = options.partialRefetch; var makeWatchQueryOptions = createMakeWatchQueryOptions(client, query, options, isSyncSSR); var _a = useInternalState(client, query, options, renderPromises, makeWatchQueryOptions), _b = _a[0], observable = _b.observable, resultData = _b.resultData, onQueryExecuted = _a[1]; var watchQueryOptions = makeWatchQueryOptions(observable); useResubscribeIfNecessary(resultData, // might get mutated during render observable, // might get mutated during render client, options, watchQueryOptions); var obsQueryFields = React.useMemo(function () { return bindObservableMethods(observable); }, [observable]); useRegisterSSRObservable(observable, renderPromises, ssrAllowed); var result = useObservableSubscriptionResult(resultData, observable, client, options, watchQueryOptions, disableNetworkFetches, partialRefetch, isSyncSSR, { onCompleted: options.onCompleted || noop, onError: options.onError || noop, }); return { result: result, obsQueryFields: obsQueryFields, observable: observable, resultData: resultData, client: client, onQueryExecuted: onQueryExecuted, }; } function useObservableSubscriptionResult(resultData, observable, client, options, watchQueryOptions, disableNetworkFetches, partialRefetch, isSyncSSR, callbacks) { var callbackRef = React.useRef(callbacks); React.useEffect(function () { // Make sure state.onCompleted and state.onError always reflect the latest // options.onCompleted and options.onError callbacks provided to useQuery, // since those functions are often recreated every time useQuery is called. // Like the forceUpdate method, the versions of these methods inherited from // InternalState.prototype are empty no-ops, but we can override them on the // base state object (without modifying the prototype). callbackRef.current = callbacks; }); var resultOverride = ((isSyncSSR || disableNetworkFetches) && options.ssr === false && !options.skip) ? // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. ssrDisabledResult : options.skip || watchQueryOptions.fetchPolicy === "standby" ? // When skipping a query (ie. we're not querying for data but still want to // render children), make sure the `data` is cleared out and `loading` is // set to `false` (since we aren't loading anything). // // NOTE: We no longer think this is the correct behavior. Skipping should // not automatically set `data` to `undefined`, but instead leave the // previous data in place. In other words, skipping should not mandate that // previously received data is all of a sudden removed. Unfortunately, // changing this is breaking, so we'll have to wait until Apollo Client 4.0 // to address this. skipStandbyResult : void 0; var previousData = resultData.previousData; var currentResultOverride = React.useMemo(function () { return resultOverride && toQueryResult(resultOverride, previousData, observable, client); }, [client, observable, resultOverride, previousData]); return useSyncExternalStore(React.useCallback(function (handleStoreChange) { // reference `disableNetworkFetches` here to ensure that the rules of hooks // keep it as a dependency of this effect, even though it's not used disableNetworkFetches; if (isSyncSSR) { return function () { }; } var onNext = function () { var previousResult = resultData.current; // We use `getCurrentResult()` instead of the onNext argument because // the values differ slightly. Specifically, loading results will have // an empty object for data instead of `undefined` for some reason. var result = observable.getCurrentResult(); // Make sure we're not attempting to re-render similar results if (previousResult && previousResult.loading === result.loading && previousResult.networkStatus === result.networkStatus && equal(previousResult.data, result.data)) { return; } setResult(result, resultData, observable, client, partialRefetch, handleStoreChange, callbackRef.current); }; var onError = function (error) { subscription.current.unsubscribe(); subscription.current = observable.resubscribeAfterError(onNext, onError); if (!hasOwnProperty.call(error, "graphQLErrors")) { // The error is not a GraphQL error throw error; } var previousResult = resultData.current; if (!previousResult || (previousResult && previousResult.loading) || !equal(error, previousResult.error)) { setResult({ data: (previousResult && previousResult.data), error: error, loading: false, networkStatus: NetworkStatus.error, }, resultData, observable, client, partialRefetch, handleStoreChange, callbackRef.current); } }; // TODO evaluate if we keep this in // React Compiler cannot handle scoped `let` access, but a mutable object // like this is fine. // was: // let subscription = observable.subscribe(onNext, onError); var subscription = { current: observable.subscribe(onNext, onError) }; // Do the "unsubscribe" with a short delay. // This way, an existing subscription can be reused without an additional // request if "unsubscribe" and "resubscribe" to the same ObservableQuery // happen in very fast succession. return function () { setTimeout(function () { return subscription.current.unsubscribe(); }); }; }, [ disableNetworkFetches, isSyncSSR, observable, resultData, partialRefetch, client, ]), function () { return currentResultOverride || getCurrentResult(resultData, observable, callbackRef.current, partialRefetch, client); }, function () { return currentResultOverride || getCurrentResult(resultData, observable, callbackRef.current, partialRefetch, client); }); } function useRegisterSSRObservable(observable, renderPromises, ssrAllowed) { if (renderPromises && ssrAllowed) { renderPromises.registerSSRObservable(observable); if (observable.getCurrentResult().loading) { // TODO: This is a legacy API which could probably be cleaned up renderPromises.addObservableQueryPromise(observable); } } } // this hook is not compatible with any rules of React, and there's no good way to rewrite it. // it should stay a separate hook that will not be optimized by the compiler function useResubscribeIfNecessary( /** this hook will mutate properties on `resultData` */ resultData, /** this hook will mutate properties on `observable` */ observable, client, options, watchQueryOptions) { var _a; if (observable[lastWatchOptions] && !equal(observable[lastWatchOptions], watchQueryOptions)) { // Though it might be tempting to postpone this reobserve call to the // useEffect block, we need getCurrentResult to return an appropriate // loading:true result synchronously (later within the same call to // useQuery). Since we already have this.observable here (not true for // the very first call to useQuery), we are not initiating any new // subscriptions, though it does feel less than ideal that reobserve // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. observable.reobserve(getObsQueryOptions(observable, client, options, watchQueryOptions)); // Make sure getCurrentResult returns a fresh ApolloQueryResult<TData>, // but save the current data as this.previousData, just like setResult // usually does. resultData.previousData = ((_a = resultData.current) === null || _a === void 0 ? void 0 : _a.data) || resultData.previousData; resultData.current = void 0; } observable[lastWatchOptions] = watchQueryOptions; } /* * A function to massage options before passing them to ObservableQuery. * This is two-step curried because we want to reuse the `make` function, * but the `observable` might differ between calls to `make`. */ export function createMakeWatchQueryOptions(client, query, _a, isSyncSSR) { if (_a === void 0) { _a = {}; } var skip = _a.skip, ssr = _a.ssr, onCompleted = _a.onCompleted, onError = _a.onError, defaultOptions = _a.defaultOptions, // The above options are useQuery-specific, so this ...otherOptions spread // makes otherOptions almost a WatchQueryOptions object, except for the // query property that we add below. otherOptions = __rest(_a, ["skip", "ssr", "onCompleted", "onError", "defaultOptions"]); return function (observable) { // This Object.assign is safe because otherOptions is a fresh ...rest object // that did not exist until just now, so modifications are still allowed. var watchQueryOptions = Object.assign(otherOptions, { query: query }); if (isSyncSSR && (watchQueryOptions.fetchPolicy === "network-only" || watchQueryOptions.fetchPolicy === "cache-and-network")) { // this behavior was added to react-apollo without explanation in this PR // https://github.com/apollographql/react-apollo/pull/1579 watchQueryOptions.fetchPolicy = "cache-first"; } if (!watchQueryOptions.variables) { watchQueryOptions.variables = {}; } if (skip) { // When skipping, we set watchQueryOptions.fetchPolicy initially to // "standby", but we also need/want to preserve the initial non-standby // fetchPolicy that would have been used if not skipping. watchQueryOptions.initialFetchPolicy = watchQueryOptions.initialFetchPolicy || watchQueryOptions.fetchPolicy || getDefaultFetchPolicy(defaultOptions, client.defaultOptions); watchQueryOptions.fetchPolicy = "standby"; } else if (!watchQueryOptions.fetchPolicy) { watchQueryOptions.fetchPolicy = (observable === null || observable === void 0 ? void 0 : observable.options.initialFetchPolicy) || getDefaultFetchPolicy(defaultOptions, client.defaultOptions); } return watchQueryOptions; }; } export function getObsQueryOptions(observable, client, queryHookOptions, watchQueryOptions) { var toMerge = []; var globalDefaults = client.defaultOptions.watchQuery; if (globalDefaults) toMerge.push(globalDefaults); if (queryHookOptions.defaultOptions) { toMerge.push(queryHookOptions.defaultOptions); } // We use compact rather than mergeOptions for this part of the merge, // because we want watchQueryOptions.variables (if defined) to replace // this.observable.options.variables whole. This replacement allows // removing variables by removing them from the variables input to // useQuery. If the variables were always merged together (rather than // replaced), there would be no way to remove existing variables. // However, the variables from options.defaultOptions and globalDefaults // (if provided) should be merged, to ensure individual defaulted // variables always have values, if not otherwise defined in // observable.options or watchQueryOptions. toMerge.push(compact(observable && observable.options, watchQueryOptions)); return toMerge.reduce(mergeOptions); } function setResult(nextResult, resultData, observable, client, partialRefetch, forceUpdate, callbacks) { var previousResult = resultData.current; if (previousResult && previousResult.data) { resultData.previousData = previousResult.data; } if (!nextResult.error && isNonEmptyArray(nextResult.errors)) { // Until a set naming convention for networkError and graphQLErrors is // decided upon, we map errors (graphQLErrors) to the error options. // TODO: Is it possible for both result.error and result.errors to be // defined here? nextResult.error = new ApolloError({ graphQLErrors: nextResult.errors }); } resultData.current = toQueryResult(unsafeHandlePartialRefetch(nextResult, observable, partialRefetch), resultData.previousData, observable, client); // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. forceUpdate(); handleErrorOrCompleted(nextResult, previousResult === null || previousResult === void 0 ? void 0 : previousResult.networkStatus, callbacks); } function handleErrorOrCompleted(result, previousNetworkStatus, callbacks) { if (!result.loading) { var error_1 = toApolloError(result); // wait a tick in case we are in the middle of rendering a component Promise.resolve() .then(function () { if (error_1) { callbacks.onError(error_1); } else if (result.data && previousNetworkStatus !== result.networkStatus && result.networkStatus === NetworkStatus.ready) { callbacks.onCompleted(result.data); } }) .catch(function (error) { globalThis.__DEV__ !== false && invariant.warn(error); }); } } function getCurrentResult(resultData, observable, callbacks, partialRefetch, client) { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. if (!resultData.current) { // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION // this could call unsafeHandlePartialRefetch setResult(observable.getCurrentResult(), resultData, observable, client, partialRefetch, function () { }, callbacks); } return resultData.current; } export function getDefaultFetchPolicy(queryHookDefaultOptions, clientDefaultOptions) { var _a; return ((queryHookDefaultOptions === null || queryHookDefaultOptions === void 0 ? void 0 : queryHookDefaultOptions.fetchPolicy) || ((_a = clientDefaultOptions === null || clientDefaultOptions === void 0 ? void 0 : clientDefaultOptions.watchQuery) === null || _a === void 0 ? void 0 : _a.fetchPolicy) || "cache-first"); } export function toApolloError(result) { return isNonEmptyArray(result.errors) ? new ApolloError({ graphQLErrors: result.errors }) : result.error; } export function toQueryResult(result, previousData, observable, client) { var data = result.data, partial = result.partial, resultWithoutPartial = __rest(result, ["data", "partial"]); var queryResult = __assign(__assign({ data: data }, resultWithoutPartial), { client: client, observable: observable, variables: observable.variables, called: result !== ssrDisabledResult && result !== skipStandbyResult, previousData: previousData }); return queryResult; } function unsafeHandlePartialRefetch(result, observable, partialRefetch) { // TODO: This code should be removed when the partialRefetch option is // removed. I was unable to get this hook to behave reasonably in certain // edge cases when this block was put in an effect. if (result.partial && partialRefetch && !result.loading && (!result.data || Object.keys(result.data).length === 0) && observable.options.fetchPolicy !== "cache-only") { observable.refetch(); return __assign(__assign({}, result), { loading: true, networkStatus: NetworkStatus.refetch }); } return result; } var ssrDisabledResult = maybeDeepFreeze({ loading: true, data: void 0, error: void 0, networkStatus: NetworkStatus.loading, }); var skipStandbyResult = maybeDeepFreeze({ loading: false, data: void 0, error: void 0, networkStatus: NetworkStatus.ready, }); function bindObservableMethods(observable) { return { refetch: observable.refetch.bind(observable), reobserve: observable.reobserve.bind(observable), fetchMore: observable.fetchMore.bind(observable), updateQuery: observable.updateQuery.bind(observable), startPolling: observable.startPolling.bind(observable), stopPolling: observable.stopPolling.bind(observable), subscribeToMore: observable.subscribeToMore.bind(observable), }; } //# sourceMappingURL=useQuery.js.map