Repository: apollographql/apollo-client
Pull request #12495: Refactor away `getObsQueryOptions`
Author: login: jerelmiller
Labels: name: auto-cleanup
description: 🤖
Created: 2025-03-28T04:14:51Z | Merged: 2025-03-28T16:18:24Z

Part 2 of refactoring `useQuery`.

This one works to remove `getObsQueryOptions` by merging the global defaults right away rather than doing it each time `watchQueryOptions` are about to get used. This simplifies and removes the need for `getObsQueryOptions`.

Best to review this one commit by commit


Diff:
diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts
index 4993e65ab08..9ad2890a49c 100644
--- a/src/react/hooks/useQuery.ts
+++ b/src/react/hooks/useQuery.ts
@@ -38,11 +38,7 @@ import { NetworkStatus } from "@apollo/client/core";
 import type { MaybeMasked, Unmasked } from "@apollo/client/masking";
 import { DocumentType, verifyDocumentType } from "@apollo/client/react/parser";
 import type { NoInfer } from "@apollo/client/utilities";
-import {
-  compact,
-  maybeDeepFreeze,
-  mergeOptions,
-} from "@apollo/client/utilities";
+import { maybeDeepFreeze, mergeOptions } from "@apollo/client/utilities";
 
 import type { NextFetchPolicyContext } from "../../core/watchQueryOptions.js";
 
@@ -275,9 +271,7 @@ function useInternalState<TData, TVariables extends OperationVariables>(
     const internalState: InternalState<TData, TVariables> = {
       client,
       query,
-      observable: client.watchQuery(
-        getObsQueryOptions(void 0, client, watchQueryOptions)
-      ),
+      observable: client.watchQuery(watchQueryOptions),
       resultData: {
         // Reuse previousData from previous InternalState (if any) to provide
         // continuity of previousData even if/when the query or client changes.
@@ -313,10 +307,10 @@ function useQueryInternals<TData, TVariables extends OperationVariables>(
   const client = useApolloClient(options.client);
   const { skip, ...otherOptions } = options;
 
-  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = {
-    ...otherOptions,
-    query,
-  };
+  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = mergeOptions(
+    client.defaultOptions.watchQuery,
+    { ...otherOptions, query }
+  );
 
   if (skip) {
     // When skipping, we set watchQueryOptions.fetchPolicy initially to
@@ -341,7 +335,6 @@ function useQueryInternals<TData, TVariables extends OperationVariables>(
   useResubscribeIfNecessary<TData, TVariables>(
     resultData, // might get mutated during render
     observable, // might get mutated during render
-    client,
     watchQueryOptions
   );
 
@@ -459,7 +452,6 @@ function useResubscribeIfNecessary<
   resultData: InternalResult<TData, TVariables>,
   /** this hook will mutate properties on `observable` */
   observable: ObsQueryWithMeta<TData, TVariables>,
-  client: ApolloClient,
   watchQueryOptions: Readonly<WatchQueryOptions<TVariables, TData>>
 ) {
   if (
@@ -474,9 +466,7 @@ function useResubscribeIfNecessary<
     // 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, watchQueryOptions)
-    );
+    observable.reobserve(watchQueryOptions);
 
     // Make sure getCurrentResult returns a fresh ApolloQueryResult<TData>,
     // but save the current data as this.previousData, just like setResult
@@ -488,31 +478,6 @@ function useResubscribeIfNecessary<
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function getObsQueryOptions<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables> | undefined,
-  client: ApolloClient,
-  watchQueryOptions: Partial<WatchQueryOptions<TVariables, TData>>
-): WatchQueryOptions<TVariables, TData> {
-  const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
-
-  const globalDefaults = client.defaultOptions.watchQuery;
-  if (globalDefaults) toMerge.push(globalDefaults);
-
-  // 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) as WatchQueryOptions<TVariables, TData>;
-}
-
 function setResult<TData, TVariables extends OperationVariables>(
   nextResult: ApolloQueryResult<MaybeMasked<TData>>,
   resultData: InternalResult<TData, TVariables>,
diff --git a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap
index d5b9ad28775..62345698c97 100644
--- a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap
+++ b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap
@@ -53,7 +53,7 @@ exports[`General use should error if the variables in the mock and component do
     __typename
   }
 }
-Expected variables: {"username":"other_user","age":<undefined>}
+Expected variables: {"username":"other_user"}
 
 Failed to match 1 mock for this query. The mocked response had the following variables:
   {"username":"mock_username"}


Reviews:
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T04:17:59Z
user: login: phryneas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T10:51:26Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T15:21:04Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T15:28:21Z
user: login: phryneas
body: Looks good to me now :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T15:29:05Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T16:14:44Z

Review comments:
diff_hunk: @@ -488,31 +478,6 @@ function useResubscribeIfNecessary<
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function getObsQueryOptions<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables> | undefined,
-  client: ApolloClient,
-  watchQueryOptions: Partial<WatchQueryOptions<TVariables, TData>>
-): WatchQueryOptions<TVariables, TData> {
-  const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
-
-  const globalDefaults = client.defaultOptions.watchQuery;
-  if (globalDefaults) toMerge.push(globalDefaults);
-
-  // 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));
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: This function was merging the `observable`'s options with `watchQueryOptions`, but `reobserve` already does this options merging (with `compact` as well!) so this was unnecessary. That only left `client.defaultOptions.watchQuery` and `watchQueryOptions` to be merged, which meant this entire function could be simplified to `mergeOptions(client.defaultOptions.watchQuery, watchQueryOptions)`. Since this happens each time `getObsQueryOptions` was called, I was able to just move the global merging where `watchQueryOptions` gets created to begin with.
created_at: 2025-03-28T04:17:59Z
author_association: MEMBER
diff_hunk: @@ -488,31 +478,6 @@ function useResubscribeIfNecessary<
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function getObsQueryOptions<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables> | undefined,
-  client: ApolloClient,
-  watchQueryOptions: Partial<WatchQueryOptions<TVariables, TData>>
-): WatchQueryOptions<TVariables, TData> {
-  const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
-
-  const globalDefaults = client.defaultOptions.watchQuery;
-  if (globalDefaults) toMerge.push(globalDefaults);
-
-  // 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));
path: src/react/hooks/useQuery.ts
user: login: phryneas
body: > Since this happens each time getObsQueryOptions was called, I was able to just move the global merging where watchQueryOptions gets created to begin with.

As of this PR it seems that `client.defaultOptions.watchQuery` doesn't get merged in at all in the hook anymore.

While the core *will* add these, it will only do so for the initial `client.watchQuery` call, but not for follow-up `reobserve` calls, which means that some options might be dropped?

The question here is probably what the user expectation is - should `watchQuery.defaultOptions` only be applied on mount, or also when an option is removed from `useQuery(query, options)` that was previously present - should it be overwritten by `watchQuery.defaultOptions` in that case? I believe previously it was, now it isn't anymore.
created_at: 2025-03-28T10:51:25Z
author_association: MEMBER
diff_hunk: @@ -488,31 +478,6 @@ function useResubscribeIfNecessary<
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function getObsQueryOptions<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables> | undefined,
-  client: ApolloClient,
-  watchQueryOptions: Partial<WatchQueryOptions<TVariables, TData>>
-): WatchQueryOptions<TVariables, TData> {
-  const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
-
-  const globalDefaults = client.defaultOptions.watchQuery;
-  if (globalDefaults) toMerge.push(globalDefaults);
-
-  // 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));
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: Discussed in person. This change goes beyond the purpose of a refactor which is to keep existing functionality, so this will be reverted back to merge global defaults. We can revisit if we want to keep/change this behavior separately.
created_at: 2025-03-28T15:21:04Z
author_association: MEMBER
diff_hunk: @@ -488,31 +478,6 @@ function useResubscribeIfNecessary<
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function getObsQueryOptions<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables> | undefined,
-  client: ApolloClient,
-  watchQueryOptions: Partial<WatchQueryOptions<TVariables, TData>>
-): WatchQueryOptions<TVariables, TData> {
-  const toMerge: Array<Partial<WatchQueryOptions<TVariables, TData>>> = [];
-
-  const globalDefaults = client.defaultOptions.watchQuery;
-  if (globalDefaults) toMerge.push(globalDefaults);
-
-  // 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));
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: Added back with [c977c16](https://github.com/apollographql/apollo-client/pull/12495/commits/c977c1692ffa994be19d0732eb54ab2e4729db39)
created_at: 2025-03-28T15:28:21Z
author_association: MEMBER
diff_hunk: @@ -53,7 +53,7 @@ exports[`General use should error if the variables in the mock and component do
     __typename
   }
 }
-Expected variables: {"username":"other_user","age":<undefined>}
+Expected variables: {"username":"other_user"}
path: src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap
user: login: jerelmiller
body: After digging into this further, I'm ok with this change since `undefined` will match just fine inside `MockLink` as long as the other variables match. This is a result of calling `mergeOptions` directly instead of using `.reduce(mergeOptions)`, which will just return options when there are no defaults.
created_at: 2025-03-28T16:14:44Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-28T04:14:56Z
body: ###  ⚠️  No Changeset found

Latest commit: 0e62659a6f3664fcc154424e71fa3fab9e4a2f22

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/apollographql/apollo-client/new/jerel/refactor-use-query-2?filename=.changeset/afraid-planets-reply.md&value=---%0A%22%40apollo%2Fclient%22%3A%20patch%0A---%0A%0ARefactor%20away%20%60getObsQueryOptions%60%0A)


author_association: NONE
user: login: svc-apollo-docs
created_at: 2025-03-28T04:14:57Z
body: ### ⚠️ Docs preview not attached to branch

The preview was not built because the PR's base branch `release-4.0` is not in the list of sources.

An Apollo team member can comment one of the following commands to dictate which branch to attach the preview to:

- `!docs set-base-branch version-2.6`
- `!docs set-base-branch main`

**Build ID**: f2645ccd71ac906492e7cbd5
author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-03-28T04:16:12Z
body: 



```
npm i https://pkg.pr.new/@apollo/client@12495
```


_commit: <a href="https://github.com/apollographql/apollo-client/runs/39598484384"><code>0e62659</code></a>_

author_association: NONE
user: login: netlify[bot]
created_at: 2025-03-28T15:25:44Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *apollo-client-docs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 0e62659a6f3664fcc154424e71fa3fab9e4a2f22 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/apollo-client-docs/deploys/67e6c9fd019b490008bef299 |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-12495--apollo-client-docs.netlify.app](https://deploy-preview-12495--apollo-client-docs.netlify.app) |
|<span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](https://app.netlify.com/qr-code/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEyNDk1LS1hcG9sbG8tY2xpZW50LWRvY3MubmV0bGlmeS5hcHAifQ.w6fn4Y9YQ8izk8tlg5W9YnICAB6ZaQiaMLAjGY_mmYk)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [apollo-client-docs Preview](https://deploy-preview-12495--apollo-client-docs.netlify.app) -->
_To edit notification comments on pull requests, go to your [Netlify site configuration](https://app.netlify.com/sites/apollo-client-docs/configuration/notifications#deploy-webhooks)._
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-28T15:26:03Z
body: ## size-limit report 📦 
| Path                                                                                      | Size                |
| ----------------------------------------------------------------------------------------- | ------------------- |
| import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (CJS)              | 41.22 KB (0%)       |
| import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production) (CJS) | 36.69 KB (0%)       |
| import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"                    | 31.83 KB (0%)       |
| import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production)       | 26.86 KB (0%)       |
| import { ApolloProvider } from "@apollo/client/react"                                     | 5.17 KB (0%)        |
| import { ApolloProvider } from "@apollo/client/react" (production)                        | 960 B (0%)          |
| import { useQuery } from "@apollo/client/react"                                           | 7.05 KB (-0.45% 🔽) |
| import { useQuery } from "@apollo/client/react" (production)                              | 2.83 KB (-1.1% 🔽)  |
| import { useLazyQuery } from "@apollo/client/react"                                       | 6.39 KB (0%)        |
| import { useLazyQuery } from "@apollo/client/react" (production)                          | 2.17 KB (0%)        |
| import { useMutation } from "@apollo/client/react"                                        | 6.46 KB (0%)        |
| import { useMutation } from "@apollo/client/react" (production)                           | 2.23 KB (0%)        |
| import { useSubscription } from "@apollo/client/react"                                    | 6.83 KB (0%)        |
| import { useSubscription } from "@apollo/client/react" (production)                       | 2.62 KB (0%)        |
| import { useSuspenseQuery } from "@apollo/client/react"                                   | 8.21 KB (0%)        |
| import { useSuspenseQuery } from "@apollo/client/react" (production)                      | 4 KB (0%)           |
| import { useBackgroundQuery } from "@apollo/client/react"                                 | 8.06 KB (0%)        |
| import { useBackgroundQuery } from "@apollo/client/react" (production)                    | 3.83 KB (0%)        |
| import { useLoadableQuery } from "@apollo/client/react"                                   | 8.06 KB (0%)        |
| import { useLoadableQuery } from "@apollo/client/react" (production)                      | 3.87 KB (0%)        |
| import { useReadQuery } from "@apollo/client/react"                                       | 5.83 KB (0%)        |
| import { useReadQuery } from "@apollo/client/react" (production)                          | 1.61 KB (0%)        |
| import { useFragment } from "@apollo/client/react"                                        | 5.89 KB (0%)        |
| import { useFragment } from "@apollo/client/react" (production)                           | 1.66 KB (0%)        |
author_association: CONTRIBUTOR