Repository: apollographql/apollo-client
Pull request #12496: Refactor `useQuery`: Combine hooks
Author: login: jerelmiller
Labels: name: auto-cleanup
description: 🤖
Created: 2025-03-28T06:26:32Z | Merged: 2025-03-28T16:26:47Z

This focuses on inlining some of the extracted hooks. This enables additional refactoring where there might have been duplicated logic, or logic that was completely unused in some places.

This one is best to review commit by commit if possible to see the progression.


Diff:
diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md
index 315941633e9..16770721c13 100644
--- a/.api-reports/api-report-react.api.md
+++ b/.api-reports/api-report-react.api.md
@@ -1414,21 +1414,9 @@ export namespace useQuery {
 // @public (undocumented)
 export namespace useQuery {
     var // (undocumented)
-    ssrDisabledResult: {
-        loading: boolean;
-        data: any;
-        error: undefined;
-        networkStatus: NetworkStatus_2;
-        partial: boolean;
-    };
+    ssrDisabledResult: ApolloQueryResult_2<any>;
     var // (undocumented)
-    skipStandbyResult: {
-        loading: boolean;
-        data: any;
-        error: undefined;
-        networkStatus: NetworkStatus_2;
-        partial: boolean;
-    };
+    skipStandbyResult: ApolloQueryResult_2<any>;
 }
 
 // @public
diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md
index 8db021b80a3..2a47be7cefc 100644
--- a/.api-reports/api-report-react_hooks.api.md
+++ b/.api-reports/api-report-react_hooks.api.md
@@ -1191,21 +1191,9 @@ export namespace useQuery {
 // @public (undocumented)
 export namespace useQuery {
     var // (undocumented)
-    ssrDisabledResult: {
-        loading: boolean;
-        data: any;
-        error: undefined;
-        networkStatus: NetworkStatus_2;
-        partial: boolean;
-    };
+    ssrDisabledResult: ApolloQueryResult_2<any>;
     var // (undocumented)
-    skipStandbyResult: {
-        loading: boolean;
-        data: any;
-        error: undefined;
-        networkStatus: NetworkStatus_2;
-        partial: boolean;
-    };
+    skipStandbyResult: ApolloQueryResult_2<any>;
 }
 
 // @public
diff --git a/package-lock.json b/package-lock.json
index 9ccb0c7c810..2f7ef3e48c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,7 +65,7 @@
         "eslint-import-resolver-typescript": "3.7.0",
         "eslint-plugin-import": "2.31.0",
         "eslint-plugin-local-rules": "3.0.2",
-        "eslint-plugin-react-compiler": "19.0.0-beta-decd7b8-20250118",
+        "eslint-plugin-react-compiler": "19.0.0-beta-aeaed83-20250323",
         "eslint-plugin-react-hooks": "5.1.0",
         "eslint-plugin-testing-library": "7.1.1",
         "expect": "29.7.0",
@@ -7808,7 +7808,9 @@
       "license": "MIT"
     },
     "node_modules/eslint-plugin-react-compiler": {
-      "version": "19.0.0-beta-decd7b8-20250118",
+      "version": "19.0.0-beta-aeaed83-20250323",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-aeaed83-20250323.tgz",
+      "integrity": "sha512-zkz1RlYTt3zzo7u/O1+5GCXFf75Qwo6guOJQVqjANagc+UoiV4RHyH+HGHQ39h7Nq/QsZG8atCiUS81RMre0UQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 3eefb9b5d1d..288ee7050d2 100644
--- a/package.json
+++ b/package.json
@@ -201,7 +201,7 @@
     "eslint-import-resolver-typescript": "3.7.0",
     "eslint-plugin-import": "2.31.0",
     "eslint-plugin-local-rules": "3.0.2",
-    "eslint-plugin-react-compiler": "19.0.0-beta-decd7b8-20250118",
+    "eslint-plugin-react-compiler": "19.0.0-beta-aeaed83-20250323",
     "eslint-plugin-react-hooks": "5.1.0",
     "eslint-plugin-testing-library": "7.1.1",
     "expect": "29.7.0",
diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts
index 9ad2890a49c..aa550835654 100644
--- a/src/react/hooks/useQuery.ts
+++ b/src/react/hooks/useQuery.ts
@@ -163,17 +163,6 @@ export declare namespace useQuery {
   }
 }
 
-type InternalQueryResult<TData, TVariables extends OperationVariables> = Omit<
-  useQuery.Result<TData, TVariables>,
-  | "startPolling"
-  | "stopPolling"
-  | "subscribeToMore"
-  | "updateQuery"
-  | "refetch"
-  | "reobserve"
-  | "fetchMore"
->;
-
 const lastWatchOptions = Symbol();
 
 interface ObsQueryWithMeta<TData, TVariables extends OperationVariables>
@@ -184,7 +173,7 @@ interface ObsQueryWithMeta<TData, TVariables extends OperationVariables>
 interface InternalResult<TData, TVariables extends OperationVariables> {
   // These members are populated by getCurrentResult and setResult, and it's
   // okay/normal for them to be initially undefined.
-  current?: undefined | InternalQueryResult<TData, TVariables>;
+  current: ApolloQueryResult<TData>;
   previousData?: undefined | MaybeMasked<TData>;
 }
 
@@ -236,6 +225,7 @@ export function useQuery<
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
   options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>> = {}
 ): useQuery.Result<TData, TVariables> {
+  "use no memo";
   return wrapHook(
     "useQuery",
     // eslint-disable-next-line react-compiler/react-compiler
@@ -248,125 +238,76 @@ function useQuery_<TData, TVariables extends OperationVariables>(
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
   options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>>
 ) {
-  const result = useQueryInternals(query, options);
-  const obsQueryFields = React.useMemo(
-    () => bindObservableMethods(result.observable),
-    [result.observable]
-  );
+  const client = useApolloClient(options.client);
+  const { skip, ssr, ...opts } = options;
 
-  return React.useMemo(
-    () => ({ ...result, ...obsQueryFields }),
-    [result, obsQueryFields]
+  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = mergeOptions(
+    client.defaultOptions.watchQuery,
+    { ...opts, query }
   );
-}
 
-function useInternalState<TData, TVariables extends OperationVariables>(
-  client: ApolloClient,
-  query: DocumentNode | TypedDocumentNode<any, any>,
-  watchQueryOptions: WatchQueryOptions<TVariables, TData>
-) {
-  function createInternalState(previous?: InternalState<TData, TVariables>) {
+  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 =
+      options.initialFetchPolicy || options.fetchPolicy;
+    watchQueryOptions.fetchPolicy = "standby";
+  }
+
+  function createState(
+    previous?: InternalState<TData, TVariables>
+  ): InternalState<TData, TVariables> {
     verifyDocumentType(query, DocumentType.Query);
 
-    const internalState: InternalState<TData, TVariables> = {
+    const observable = client.watchQuery(watchQueryOptions);
+
+    return {
       client,
       query,
-      observable: client.watchQuery(watchQueryOptions),
+      observable,
       resultData: {
+        current: observable.getCurrentResult(),
         // Reuse previousData from previous InternalState (if any) to provide
         // continuity of previousData even if/when the query or client changes.
-        previousData: previous?.resultData.current?.data,
+        previousData: previous?.resultData.current.data,
       },
     };
-
-    return internalState as InternalState<TData, TVariables>;
   }
 
-  let [internalState, updateInternalState] =
-    React.useState(createInternalState);
+  let [state, setState] = React.useState(createState);
 
-  if (client !== internalState.client || query !== internalState.query) {
+  if (client !== state.client || query !== state.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.
-    const newInternalState = createInternalState(internalState);
-    updateInternalState(newInternalState);
-    return newInternalState;
+    setState((state = createState(state)));
   }
 
-  return internalState;
-}
-
-function useQueryInternals<TData, TVariables extends OperationVariables>(
-  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
-  options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>>
-) {
-  const client = useApolloClient(options.client);
-  const { skip, ...otherOptions } = options;
+  const { observable, resultData } = state;
 
-  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = mergeOptions(
-    client.defaultOptions.watchQuery,
-    { ...otherOptions, query }
+  useInitialFetchPolicyIfNecessary<TData, TVariables>(
+    watchQueryOptions,
+    observable
   );
 
-  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 =
-      options.initialFetchPolicy || options.fetchPolicy;
-    watchQueryOptions.fetchPolicy = "standby";
-  }
-
-  const { observable, resultData } = useInternalState(
-    client,
-    query,
-    watchQueryOptions
-  );
-
-  if (!watchQueryOptions.fetchPolicy) {
-    // eslint-disable-next-line react-compiler/react-compiler
-    watchQueryOptions.fetchPolicy = observable.options.initialFetchPolicy;
-  }
-
   useResubscribeIfNecessary<TData, TVariables>(
     resultData, // might get mutated during render
     observable, // might get mutated during render
     watchQueryOptions
   );
 
-  const result = useObservableSubscriptionResult<TData, TVariables>(
-    resultData,
-    observable,
-    client,
-    options,
-    watchQueryOptions
-  );
-
-  return result;
-}
-
-function useObservableSubscriptionResult<
-  TData,
-  TVariables extends OperationVariables,
->(
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient,
-  options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>>,
-  watchQueryOptions: Readonly<WatchQueryOptions<TVariables, TData>>
-) {
   const ssrDisabledOverride = useSyncExternalStore(
     () => () => {},
-    () => false,
-    () => options.ssr === false
+    () => void 0,
+    () => (ssr === false ? useQuery.ssrDisabledResult : void 0)
   );
 
   const resultOverride =
-    options.skip || watchQueryOptions.fetchPolicy === "standby" ?
+    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).
@@ -378,17 +319,61 @@ function useObservableSubscriptionResult<
       // changing this is breaking, so we'll have to wait until Apollo Client 4.0
       // to address this.
       useQuery.skipStandbyResult
-    : ssrDisabledOverride ? useQuery.ssrDisabledResult
-    : void 0;
+    : ssrDisabledOverride;
 
-  const previousData = resultData.previousData;
-  const currentResultOverride = React.useMemo(
-    () =>
-      resultOverride &&
-      toQueryResult(resultOverride, previousData, observable, client),
-    [client, observable, resultOverride, previousData]
+  const result = useResultSubscription<TData, TVariables>(
+    observable,
+    resultData,
+    resultOverride
+  );
+
+  const obsQueryFields = React.useMemo(
+    () => ({
+      refetch: observable.refetch.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),
+    }),
+    [observable]
   );
 
+  const previousData = resultData.previousData;
+  return React.useMemo(() => {
+    const { data, partial, ...rest } = result;
+
+    return {
+      data, // Ensure always defined, even if result.data is missing.
+      ...rest,
+      client: client,
+      observable: observable,
+      variables: observable.variables,
+      previousData,
+      ...obsQueryFields,
+    };
+  }, [result, client, observable, previousData, obsQueryFields]);
+}
+
+function useInitialFetchPolicyIfNecessary<
+  TData,
+  TVariables extends OperationVariables,
+>(
+  watchQueryOptions: WatchQueryOptions<TVariables, TData>,
+  observable: ObsQueryWithMeta<TData, TVariables>
+) {
+  "use no memo";
+  if (!watchQueryOptions.fetchPolicy) {
+    watchQueryOptions.fetchPolicy = observable.options.initialFetchPolicy;
+  }
+}
+
+function useResultSubscription<TData, TVariables extends OperationVariables>(
+  observable: ObsQueryWithMeta<TData, TVariables>,
+  resultData: InternalResult<TData, TVariables>,
+  resultOverride: ApolloQueryResult<any> | undefined
+) {
+  "use no memo";
   return useSyncExternalStore(
     React.useCallback(
       (handleStoreChange) => {
@@ -415,13 +400,13 @@ function useObservableSubscriptionResult<
               return;
             }
 
-            setResult(
-              result,
-              resultData,
-              observable,
-              client,
-              handleStoreChange
-            );
+            if (previousResult && previousResult.data) {
+              // eslint-disable-next-line react-compiler/react-compiler
+              resultData.previousData = previousResult.data;
+            }
+
+            resultData.current = result;
+            handleStoreChange();
           });
 
         // Do the "unsubscribe" with a short delay.
@@ -433,12 +418,10 @@ function useObservableSubscriptionResult<
         };
       },
 
-      [observable, resultData, client]
+      [observable, resultData]
     ),
-    () =>
-      currentResultOverride || getCurrentResult(resultData, observable, client),
-    () =>
-      currentResultOverride || getCurrentResult(resultData, observable, client)
+    () => resultOverride || resultData.current,
+    () => resultOverride || resultData.current
   );
 }
 
@@ -454,6 +437,7 @@ function useResubscribeIfNecessary<
   observable: ObsQueryWithMeta<TData, TVariables>,
   watchQueryOptions: Readonly<WatchQueryOptions<TVariables, TData>>
 ) {
+  "use no memo";
   if (
     observable[lastWatchOptions] &&
     !equal(observable[lastWatchOptions], watchQueryOptions)
@@ -472,80 +456,19 @@ function useResubscribeIfNecessary<
     // but save the current data as this.previousData, just like setResult
     // usually does.
     resultData.previousData =
-      resultData.current?.data || resultData.previousData;
-    resultData.current = void 0;
+      resultData.current.data || resultData.previousData;
+    resultData.current = observable.getCurrentResult();
   }
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function setResult<TData, TVariables extends OperationVariables>(
-  nextResult: ApolloQueryResult<MaybeMasked<TData>>,
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient,
-  forceUpdate: () => void
-) {
-  const previousResult = resultData.current;
-  if (previousResult && previousResult.data) {
-    resultData.previousData = previousResult.data;
-  }
-
-  resultData.current = toQueryResult(
-    nextResult,
-    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();
-}
-
-function getCurrentResult<TData, TVariables extends OperationVariables>(
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  // 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) {
-    setResult(
-      observable.getCurrentResult(),
-      resultData,
-      observable,
-      client,
-      () => {}
-    );
-  }
-  return resultData.current!;
-}
-
-function toQueryResult<TData, TVariables extends OperationVariables>(
-  result: ApolloQueryResult<MaybeMasked<TData>>,
-  previousData: MaybeMasked<TData> | undefined,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  const { data, partial, ...resultWithoutPartial } = result;
-  const queryResult: InternalQueryResult<TData, TVariables> = {
-    data, // Ensure always defined, even if result.data is missing.
-    ...resultWithoutPartial,
-    client: client,
-    observable: observable,
-    variables: observable.variables,
-    previousData,
-  };
-  return queryResult;
-}
-
 useQuery.ssrDisabledResult = maybeDeepFreeze({
   loading: true,
   data: void 0 as any,
   error: void 0,
   networkStatus: NetworkStatus.loading,
   partial: true,
-});
+}) satisfies ApolloQueryResult<any> as ApolloQueryResult<any>;
 
 useQuery.skipStandbyResult = maybeDeepFreeze({
   loading: false,
@@ -553,17 +476,4 @@ useQuery.skipStandbyResult = maybeDeepFreeze({
   error: void 0,
   networkStatus: NetworkStatus.ready,
   partial: true,
-});
-
-function bindObservableMethods<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables>
-) {
-  return {
-    refetch: observable.refetch.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),
-  };
-}
+}) satisfies ApolloQueryResult<any> as ApolloQueryResult<any>;


Reviews:
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T06:27:52Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T06:28:41Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T06:30:05Z
user: login: jerelmiller
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T06:30:49Z
user: login: phryneas
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T11:18:37Z
user: login: phryneas
body: Assuming we reinstate the compiler-compatibility, this looks good :)
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T11:38:56Z

Review comments:
diff_hunk: @@ -248,84 +237,56 @@ function useQuery_<TData, TVariables extends OperationVariables>(
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
   options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>>
 ) {
-  const result = useQueryInternals(query, options);
-  const obsQueryFields = React.useMemo(
-    () => bindObservableMethods(result.observable),
-    [result.observable]
-  );
+  const client = useApolloClient(options.client);
+  const { skip, ssr, ...opts } = options;
 
-  return React.useMemo(
-    () => ({ ...result, ...obsQueryFields }),
-    [result, obsQueryFields]
-  );
-}
+  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = {
+    ...opts,
+    query,
+  };
 
-function useInternalState<TData, TVariables extends OperationVariables>(
-  client: ApolloClient,
-  query: DocumentNode | TypedDocumentNode<any, any>,
-  watchQueryOptions: WatchQueryOptions<TVariables, TData>
-) {
-  function createInternalState(previous?: InternalState<TData, TVariables>) {
+  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 =
+      options.initialFetchPolicy || options.fetchPolicy;
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: We should consider deprecating/removing `initialFetchPolicy`. Its unclear when you'd want it over `fetchPolicy`. That would simplify this even further.
created_at: 2025-03-28T06:27:52Z
author_association: MEMBER
diff_hunk: @@ -248,84 +237,56 @@ function useQuery_<TData, TVariables extends OperationVariables>(
   query: DocumentNode | TypedDocumentNode<TData, TVariables>,
   options: useQuery.Options<NoInfer<TData>, NoInfer<TVariables>>
 ) {
-  const result = useQueryInternals(query, options);
-  const obsQueryFields = React.useMemo(
-    () => bindObservableMethods(result.observable),
-    [result.observable]
-  );
+  const client = useApolloClient(options.client);
+  const { skip, ssr, ...opts } = options;
 
-  return React.useMemo(
-    () => ({ ...result, ...obsQueryFields }),
-    [result, obsQueryFields]
-  );
-}
+  const watchQueryOptions: WatchQueryOptions<TVariables, TData> = {
+    ...opts,
+    query,
+  };
 
-function useInternalState<TData, TVariables extends OperationVariables>(
-  client: ApolloClient,
-  query: DocumentNode | TypedDocumentNode<any, any>,
-  watchQueryOptions: WatchQueryOptions<TVariables, TData>
-) {
-  function createInternalState(previous?: InternalState<TData, TVariables>) {
+  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 =
+      options.initialFetchPolicy || options.fetchPolicy;
+    watchQueryOptions.fetchPolicy = "standby";
+  }
+
+  function createState(
+    previous?: InternalState<TData, TVariables>
+  ): InternalState<TData, TVariables> {
     verifyDocumentType(query, DocumentType.Query);
 
-    const internalState: InternalState<TData, TVariables> = {
+    const observable = client.watchQuery(watchQueryOptions);
+
+    return {
       client,
       query,
-      observable: client.watchQuery(watchQueryOptions),
+      observable,
       resultData: {
+        current: observable.getCurrentResult(),
         // Reuse previousData from previous InternalState (if any) to provide
         // continuity of previousData even if/when the query or client changes.
-        previousData: previous?.resultData.current?.data,
+        previousData: previous?.resultData.current.data,
       },
     };
-
-    return internalState as InternalState<TData, TVariables>;
   }
 
-  let [internalState, updateInternalState] =
-    React.useState(createInternalState);
+  let [state, setState] = React.useState(createState);
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: With `useInternalState` now inlined, I decided to rename this more plainly since its technically all "internal" state at this point.
created_at: 2025-03-28T06:28:40Z
author_association: MEMBER
diff_hunk: @@ -433,13 +363,38 @@ function useObservableSubscriptionResult<
         };
       },
 
-      [observable, resultData, client]
+      [observable, resultData]
     ),
-    () =>
-      currentResultOverride || getCurrentResult(resultData, observable, client),
-    () =>
-      currentResultOverride || getCurrentResult(resultData, observable, client)
+    () => resultOverride || resultData.current,
+    () => resultOverride || resultData.current
   );
+
+  const obsQueryFields = React.useMemo(
+    () => ({
+      refetch: observable.refetch.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),
+    }),
+    [observable]
+  );
+
+  const previousData = resultData.previousData;
+  return React.useMemo(() => {
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: Moving code around allowed me to move `toQueryResult` here, which I then just inlined. We were using `toQueryResult` on the override itself, then the uSES value. Now it just takes the raw query result and applies the extra fields at the end before returning.
created_at: 2025-03-28T06:30:05Z
author_association: MEMBER
path: src/react/hooks/useQuery.ts
user: login: jerelmiller
body: Between all 3 refactors, I've cut almost 200 lines of code!
created_at: 2025-03-28T06:30:49Z
author_association: MEMBER
diff_hunk: @@ -472,98 +456,24 @@ function useResubscribeIfNecessary<
     // but save the current data as this.previousData, just like setResult
     // usually does.
     resultData.previousData =
-      resultData.current?.data || resultData.previousData;
-    resultData.current = void 0;
+      resultData.current.data || resultData.previousData;
+    resultData.current = observable.getCurrentResult();
   }
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function setResult<TData, TVariables extends OperationVariables>(
-  nextResult: ApolloQueryResult<MaybeMasked<TData>>,
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient,
-  forceUpdate: () => void
-) {
-  const previousResult = resultData.current;
-  if (previousResult && previousResult.data) {
-    resultData.previousData = previousResult.data;
-  }
-
-  resultData.current = toQueryResult(
-    nextResult,
-    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();
-}
-
-function getCurrentResult<TData, TVariables extends OperationVariables>(
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  // 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) {
-    setResult(
-      observable.getCurrentResult(),
-      resultData,
-      observable,
-      client,
-      () => {}
-    );
-  }
-  return resultData.current!;
-}
-
-function toQueryResult<TData, TVariables extends OperationVariables>(
-  result: ApolloQueryResult<MaybeMasked<TData>>,
-  previousData: MaybeMasked<TData> | undefined,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  const { data, partial, ...resultWithoutPartial } = result;
-  const queryResult: InternalQueryResult<TData, TVariables> = {
-    data, // Ensure always defined, even if result.data is missing.
-    ...resultWithoutPartial,
-    client: client,
-    observable: observable,
-    variables: observable.variables,
-    previousData,
-  };
-  return queryResult;
-}
-
 useQuery.ssrDisabledResult = maybeDeepFreeze({
   loading: true,
   data: void 0 as any,
   error: void 0,
   networkStatus: NetworkStatus.loading,
   partial: true,
-});
+}) as ApolloQueryResult<any>;
path: src/react/hooks/useQuery.ts
user: login: phryneas
body: 
```suggestion
}) satisfies ApolloQueryResult<any> as ApolloQueryResult<any>;
```

To ensure this is not a cast.
created_at: 2025-03-28T11:18:36Z
author_association: MEMBER
diff_hunk: @@ -472,98 +456,24 @@ function useResubscribeIfNecessary<
     // but save the current data as this.previousData, just like setResult
     // usually does.
     resultData.previousData =
-      resultData.current?.data || resultData.previousData;
-    resultData.current = void 0;
+      resultData.current.data || resultData.previousData;
+    resultData.current = observable.getCurrentResult();
   }
   observable[lastWatchOptions] = watchQueryOptions;
 }
 
-function setResult<TData, TVariables extends OperationVariables>(
-  nextResult: ApolloQueryResult<MaybeMasked<TData>>,
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient,
-  forceUpdate: () => void
-) {
-  const previousResult = resultData.current;
-  if (previousResult && previousResult.data) {
-    resultData.previousData = previousResult.data;
-  }
-
-  resultData.current = toQueryResult(
-    nextResult,
-    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();
-}
-
-function getCurrentResult<TData, TVariables extends OperationVariables>(
-  resultData: InternalResult<TData, TVariables>,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  // 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) {
-    setResult(
-      observable.getCurrentResult(),
-      resultData,
-      observable,
-      client,
-      () => {}
-    );
-  }
-  return resultData.current!;
-}
-
-function toQueryResult<TData, TVariables extends OperationVariables>(
-  result: ApolloQueryResult<MaybeMasked<TData>>,
-  previousData: MaybeMasked<TData> | undefined,
-  observable: ObservableQuery<TData, TVariables>,
-  client: ApolloClient
-): InternalQueryResult<TData, TVariables> {
-  const { data, partial, ...resultWithoutPartial } = result;
-  const queryResult: InternalQueryResult<TData, TVariables> = {
-    data, // Ensure always defined, even if result.data is missing.
-    ...resultWithoutPartial,
-    client: client,
-    observable: observable,
-    variables: observable.variables,
-    previousData,
-  };
-  return queryResult;
-}
-
 useQuery.ssrDisabledResult = maybeDeepFreeze({
   loading: true,
   data: void 0 as any,
   error: void 0,
   networkStatus: NetworkStatus.loading,
   partial: true,
-});
+}) as ApolloQueryResult<any>;
 
 useQuery.skipStandbyResult = maybeDeepFreeze({
   loading: false,
   data: void 0 as any,
   error: void 0,
   networkStatus: NetworkStatus.ready,
   partial: true,
-});
-
-function bindObservableMethods<TData, TVariables extends OperationVariables>(
-  observable: ObservableQuery<TData, TVariables>
-) {
-  return {
-    refetch: observable.refetch.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),
-  };
-}
+}) as ApolloQueryResult<any>;
path: src/react/hooks/useQuery.ts
user: login: phryneas
body: 
```suggestion
}) satisfies ApolloQueryResult<any> as ApolloQueryResult<any>;
```
created_at: 2025-03-28T11:18:43Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-28T06:26:36Z
body: ###  ⚠️  No Changeset found

Latest commit: 887aa442c445785d2c456c00fb887496f8e7804d

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-3?filename=.changeset/big-rockets-rule.md&value=---%0A%22%40apollo%2Fclient%22%3A%20patch%0A---%0A%0ARefactor%20%60useQuery%60%3A%20Combine%20hooks%0A)


author_association: NONE
user: login: svc-apollo-docs
created_at: 2025-03-28T06:26:37Z
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**: 6a8c6bbd68a002abb4a72baf
author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-03-28T06:27:52Z
body: 



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


_commit: <a href="https://github.com/apollographql/apollo-client/runs/39599026228"><code>887aa44</code></a>_

author_association: NONE
user: login: phryneas
created_at: 2025-03-28T09:21:19Z
body: While I love the refactor, some of these moves now move "compiler-breaking" code into hooks compilable parent hooks and as a result prevent any optimization with the React compiler.

In short, these two pieces of code have to be their own hooks:

https://github.com/apollographql/apollo-client/blob/1c01a4bf97ca45853476c0f41c6f1b7699d70d06/src/react/hooks/useQuery.ts#L323-L371

https://github.com/apollographql/apollo-client/blob/1c01a4bf97ca45853476c0f41c6f1b7699d70d06/src/react/hooks/useQuery.ts#L291-L294

Still digging a bit further...
author_association: MEMBER
user: login: phryneas
created_at: 2025-03-28T09:47:09Z
body: I've pushed [36d30f8](https://github.com/apollographql/apollo-client/pull/12496/commits/36d30f8358e1d38cf4c71c38d1d53fd55d796810) to move a few things out again - this would work with the React compiler again. WDYT?
author_association: MEMBER
user: login: jerelmiller
created_at: 2025-03-28T16:20:09Z
body: TIL about "use no memo"
author_association: MEMBER
user: login: netlify[bot]
created_at: 2025-03-28T16:21:09Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *apollo-client-docs* ready!


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 887aa442c445785d2c456c00fb887496f8e7804d |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/apollo-client-docs/deploys/67e6cc11d49e140008caa03b |
|<span aria-hidden="true">😎</span> Deploy Preview | [https://deploy-preview-12496--apollo-client-docs.netlify.app](https://deploy-preview-12496--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.eyJ1cmwiOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTEyNDk2LS1hcG9sbG8tY2xpZW50LWRvY3MubmV0bGlmeS5hcHAifQ.8lnpaaKF7CXLS3WTXZqiLhKhs1FPmHebBskg54gE7kM)<br /><br />_Use your smartphone camera to open QR code link._</details> |
---
<!-- [apollo-client-docs Preview](https://deploy-preview-12496--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-28T16:21:16Z
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"                                           | 6.99 KB (-0.89% 🔽) |
| import { useQuery } from "@apollo/client/react" (production)                              | 2.76 KB (-2.56% 🔽) |
| 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