Repository: apollographql/apollo-client

Problem statement:
Pagination using useSuspenseQuery and React transition is broken
I have a paginated query that I use with `useSuspenseQuery`. 
When I call the `fetchMore` function inside a `startTransition`, an unexpected re-render with **only** the new data happens before the final re-render with all the data merged together.

I've made a reproduction link so you can try it out : https://stackblitz.com/edit/stackblitz-starters-fqmtck?file=src%2Fcomponents%2FPaginatedQuery.tsx

### Reproduction steps

1 - Go to the stackblitz link
2 - Open your console
3 - Click "Fetch More"
4 - Look at logs


Hints:
Thanks for reporting this, @pvandamme! Really appreciate the reproduction - looks like this issue should be transferred over to the Apollo Client repo which I will do shortly.
Thanks @alessbell :) 
Hi @alessbell, any issue that I can follow on the apollo client repo for updates ?
Thanks !

Faced a similar issue. 
Seems to be caused by [this line](https://github.com/apollographql/apollo-client/blob/0bcbb2c258c5771e6bb170c59a9ce360645351b4/src/react/cache/QueryReference.ts#L303)
Hi there, I'm sorry - it seems that something went wrong when transferring this earlier. I transferred it now.
Is there any workaround available currently?
@finkef 
Well, with @apollo/client v3.9.5, deleting line 243 in `node_modules/@apollo/client/react/internal/cache/QueryReference.js`  seems to fix the issue for me.
But I wouldn't do it in an app I care about)
@none23 Thanks!

I just came up with this workaround, but wouldn't expect the best performance. It appears that `readQuery` always gets the correct data.

```ts
import { useApolloClient } from "@apollo/client"
import { useSuspenseQuery as useApolloSuspenseQuery } from "@apollo/experimental-nextjs-app-support/ssr"
import { startTransition, useCallback, useMemo } from "react"

export const useSuspenseQuery: typeof useApolloSuspenseQuery = (
  ...args: Parameters<typeof useApolloSuspenseQuery>
) => {
  const client = useApolloClient()
  const { data, fetchMore, ...rest } = useApolloSuspenseQuery(...args)

  const newFetchMore = useCallback(
    (...fetchMoreArgs: Parameters<typeof fetchMore>) => {
      return new Promise((resolve, reject) => {
        startTransition(() => {
          fetchMore(...fetchMoreArgs)
            .then(resolve)
            .catch(reject)
        })
      })
    },
    [fetchMore],
  )

  const actualData = useMemo(() => {
    return client.readQuery({
      query: args[0],
      variables: (args as any)[1]?.variables,
    })
  }, [data])

  return {
    data: actualData,
    fetchMore: newFetchMore,
    ...rest,
  } as any
}

```

Patch:
diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts
--- a/src/react/internal/cache/QueryReference.ts
+++ b/src/react/internal/cache/QueryReference.ts
@@ -379,10 +379,22 @@ export class InternalQueryReference<TData = unknown> {
     // promise is resolved correctly.
     returnedPromise
       .then((result) => {
-        if (this.promise.status === "pending") {
-          this.result = result;
-          this.resolve?.(result);
-        }
+        // In the case of `fetchMore`, this promise is resolved before a cache
+        // result is emitted due to the fact that `fetchMore` sets a `no-cache`
+        // fetch policy and runs `cache.batch` in its `.then` handler. Because
+        // the timing is different, we accidentally run this update twice
+        // causing an additional re-render with the `fetchMore` result by
+        // itself. By wrapping in `setTimeout`, this should provide a short
+        // delay to allow the `QueryInfo.notify` handler to run before this
+        // promise is checked.
+        // See https://github.com/apollographql/apollo-client/issues/11315 for
+        // more information
+        setTimeout(() => {
+          if (this.promise.status === "pending") {
+            this.result = result;
+            this.resolve?.(result);
+          }
+        });
       })
       .catch(() => {});
 


Test patch:
diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx
--- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx
+++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx
@@ -4927,25 +4927,6 @@ describe("fetchMore", () => {
       expect(renderedComponents).toStrictEqual([App, SuspenseFallback]);
     }
 
-    // TODO: Determine why we have this extra render here.
-    // Possibly related: https://github.com/apollographql/apollo-client/issues/11315
-    {
-      const { snapshot } = await Profiler.takeRender();
-
-      expect(snapshot.result).toEqual({
-        data: {
-          letters: [
-            { __typename: "Letter", position: 1, letter: "A" },
-            { __typename: "Letter", position: 2, letter: "B" },
-            { __typename: "Letter", position: 3, letter: "C" },
-            { __typename: "Letter", position: 4, letter: "D" },
-          ],
-        },
-        error: undefined,
-        networkStatus: NetworkStatus.ready,
-      });
-    }
-
     {
       const { snapshot } = await Profiler.takeRender();
 
@@ -5034,25 +5015,6 @@ describe("fetchMore", () => {
       expect(renderedComponents).toStrictEqual([App, SuspenseFallback]);
     }
 
-    // TODO: Determine why we have this extra render here.
-    // Possibly related: https://github.com/apollographql/apollo-client/issues/11315
-    {
-      const { snapshot } = await Profiler.takeRender();
-
-      expect(snapshot.result).toEqual({
-        data: {
-          letters: [
-            { __typename: "Letter", position: 1, letter: "A" },
-            { __typename: "Letter", position: 2, letter: "B" },
-            { __typename: "Letter", position: 3, letter: "C" },
-            { __typename: "Letter", position: 4, letter: "D" },
-          ],
-        },
-        error: undefined,
-        networkStatus: NetworkStatus.ready,
-      });
-    }
-
     {
       const { snapshot } = await Profiler.takeRender();
 
@@ -5245,39 +5207,6 @@ describe("fetchMore", () => {
       });
     }
 
-    // TODO: Determine why we have this extra render here. This should mimic
-    // the update in the next render where we see <App /> included in the
-    // rerendered components.
-    // Possibly related: https://github.com/apollographql/apollo-client/issues/11315
-    {
-      const { snapshot, renderedComponents } = await Profiler.takeRender();
-
-      expect(renderedComponents).toStrictEqual([ReadQueryHook]);
-      expect(snapshot).toEqual({
-        isPending: false,
-        result: {
-          data: {
-            todos: [
-              {
-                __typename: "Todo",
-                id: "1",
-                name: "Clean room",
-                completed: false,
-              },
-              {
-                __typename: "Todo",
-                id: "2",
-                name: "Take out trash",
-                completed: true,
-              },
-            ],
-          },
-          error: undefined,
-          networkStatus: NetworkStatus.ready,
-        },
-      });
-    }
-
     {
       // Eventually we should see the updated todos content once its done
       // suspending.
diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx
--- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx
+++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx
@@ -1,4 +1,4 @@
-import React, { Fragment, StrictMode, Suspense } from "react";
+import React, { Fragment, StrictMode, Suspense, useTransition } from "react";
 import {
   act,
   screen,
@@ -51,7 +51,15 @@ import {
   RefetchWritePolicy,
   WatchQueryFetchPolicy,
 } from "../../../core/watchQueryOptions";
-import { profile, spyOnConsole } from "../../../testing/internal";
+import {
+  PaginatedCaseData,
+  PaginatedCaseVariables,
+  createProfiler,
+  profile,
+  setupPaginatedCase,
+  spyOnConsole,
+  useTrackRenders,
+} from "../../../testing/internal";
 
 type RenderSuspenseHookOptions<Props, TSerializedCache = {}> = Omit<
   RenderHookOptions<Props>,
@@ -9978,6 +9986,129 @@ describe("useSuspenseQuery", () => {
     });
   });
 
+  // https://github.com/apollographql/apollo-client/issues/11315
+  it("fetchMore does not cause extra render", async () => {
+    const { query, link } = setupPaginatedCase();
+
+    const user = userEvent.setup();
+    const client = new ApolloClient({
+      cache: new InMemoryCache({
+        typePolicies: {
+          Query: {
+            fields: {
+              letters: offsetLimitPagination(),
+            },
+          },
+        },
+      }),
+      link,
+    });
+
+    const Profiler = createProfiler({
+      initialSnapshot: {
+        result: null as UseSuspenseQueryResult<
+          PaginatedCaseData,
+          PaginatedCaseVariables
+        > | null,
+      },
+    });
+
+    function SuspenseFallback() {
+      useTrackRenders();
+
+      return <div>Loading...</div>;
+    }
+
+    function App() {
+      useTrackRenders();
+      const [isPending, startTransition] = useTransition();
+      const result = useSuspenseQuery(query, {
+        variables: { offset: 0, limit: 2 },
+      });
+      const { data, fetchMore } = result;
+
+      Profiler.mergeSnapshot({ result });
+
+      return (
+        <button
+          disabled={isPending}
+          onClick={() =>
+            startTransition(() => {
+              fetchMore({
+                variables: {
+                  offset: data.letters.length,
+                  limit: data.letters.length + 1,
+                },
+              });
+            })
+          }
+        >
+          Fetch next
+        </button>
+      );
+    }
+
+    render(<App />, {
+      wrapper: ({ children }) => (
+        <ApolloProvider client={client}>
+          <Profiler>
+            <Suspense fallback={<SuspenseFallback />}>{children}</Suspense>
+          </Profiler>
+        </ApolloProvider>
+      ),
+    });
+
+    {
+      const { renderedComponents } = await Profiler.takeRender();
+
+      expect(renderedComponents).toStrictEqual([SuspenseFallback]);
+    }
+
+    {
+      const { snapshot, renderedComponents } = await Profiler.takeRender();
+
+      expect(renderedComponents).toStrictEqual([App]);
+      expect(snapshot.result?.data).toEqual({
+        letters: [
+          { __typename: "Letter", letter: "A", position: 1 },
+          { __typename: "Letter", letter: "B", position: 2 },
+        ],
+      });
+    }
+
+    await act(() => user.click(screen.getByText("Fetch next")));
+
+    {
+      const { snapshot, renderedComponents } = await Profiler.takeRender();
+
+      expect(renderedComponents).toStrictEqual([App]);
+      expect(screen.getByText("Fetch next")).toBeDisabled();
+      expect(snapshot.result?.data).toEqual({
+        letters: [
+          { __typename: "Letter", letter: "A", position: 1 },
+          { __typename: "Letter", letter: "B", position: 2 },
+        ],
+      });
+    }
+
+    {
+      const { snapshot, renderedComponents } = await Profiler.takeRender();
+
+      expect(renderedComponents).toStrictEqual([App]);
+      expect(snapshot.result?.data).toEqual({
+        letters: [
+          { __typename: "Letter", letter: "A", position: 1 },
+          { __typename: "Letter", letter: "B", position: 2 },
+          { __typename: "Letter", letter: "C", position: 3 },
+          { __typename: "Letter", letter: "D", position: 4 },
+          { __typename: "Letter", letter: "E", position: 5 },
+        ],
+      });
+    }
+
+    await expect(Profiler).not.toRerender();
+  });
+
   describe.skip("type tests", () => {
     it("returns unknown when TData cannot be inferred", () => {
       const query = gql`
diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts
--- a/src/testing/internal/scenarios/index.ts
+++ b/src/testing/internal/scenarios/index.ts
@@ -80,8 +80,8 @@ export interface PaginatedCaseVariables {
 export function setupPaginatedCase() {
   const query: TypedDocumentNode<PaginatedCaseData, PaginatedCaseVariables> =
     gql`
-      query letters($limit: Int, $offset: Int) {
-        letters(limit: $limit) {
+      query LettersQuery($limit: Int, $offset: Int) {
+        letters(limit: $limit, offset: $offset) {
           letter
           position
         }