apollographql / apollographql/apollo-client-integrations

SimulatePreloadedQuery fires spurious network request due to useBackgroundQuery racing with stream transport

Open
#546 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
556
Forks
53
PR merge metrics
No merged PRs in 30d

Description

---
Package: @apollo/client-react-streaming v0.14.4 (via
@apollo/client-integration-nextjs v0.14.4)

Apollo Client: v4.1.6

---
Description

When using PreloadQuery from registerApolloClient with Next.js App
Router, the client always fires an additional network request after SSR
hydration; even though the server already fetched the data and
transported it via the RSC stream.

**Expected behavior**: The server fetches via PreloadQuery, the stream
transports the result to the client, and useSuspenseQuery (or
useReadQuery) reads from cache without a network request.

**Actual behavior:** A duplicate GraphQL request appears in the browser
Network tab on every page load.

---
Root cause

In SimulatePreloadedQuery.cc.js:

```
function SimulatePreloadedQuery({ queryRef, children }) {
const client = useApolloClient2();
reviveTransportedQueryRef(queryRef, client); // (1)
const bgQueryArgs = useMemo2(() => {
const { query, ...hydratedOptions } = deserializeOptions(
queryRef.$__apollo_queryRef.options
);
return [
query,
{ ...hydratedOptions, queryKey:
queryRef.$__apollo_queryRef.queryKey }
];
}, [queryRef.$__apollo_queryRef]);
useBackgroundQuery(...bgQueryArgs); // (2)
return children;
}
```

(1) reviveTransportedQueryRef creates a watchQuery observable that
reads from the transported ReadableStream via
ReadFromReadableStreamLink. This is async — the stream data arrives
over time as the RSC payload streams in.

(2) useBackgroundQuery runs synchronously after revival. It creates a
new, separate watchQuery observable with cache-first. At this point the
stream from (1) hasn't delivered data to the cache yet, so the cache
is empty. cache-first on an empty cache initiates a network request.

Additionally, useBackgroundQuery is called with queryKey:
queryRef.$__apollo_queryRef.queryKey (a UUID generated by
crypto.randomUUID()). This means:
- useBackgroundQuery creates a suspense cache entry keyed by [query,
variables, UUID]
- useSuspenseQuery in child components (without queryKey) looks up
[query, variables, undefined]
- These are different entries — so useSuspenseQuery also creates its
own watchQuery and fires yet another network request

The net result: 1 server fetch + 1-2 client fetches for the same query.

---
Reproduction

```
// page.tsx (React Server Component)
import { PreloadQuery } from "@/graphql/client"; // from
registerApolloClient

export default async function Page() {
return (



);
}
```

```
// ClientChild.tsx ("use client")
import { useSuspenseQuery } from "@apollo/client/react";

export function ClientChild() {
const { data } = useSuspenseQuery(MY_QUERY, {
variables: { id: "1" },
});
return

{data.name}
;
}
```

Open the page → observe the Network tab: MY_QUERY fires as a
client-side request after the SSR-rendered content is already visible.

---
Suggested fix

useBackgroundQuery inside SimulatePreloadedQuery should not create a
new watchQuery observable. Instead, it should reuse the
InternalQueryReference already created by reviveTransportedQueryRef —
which is backed by the stream-reading observable and won't fire a
network request.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in SimulatePreloadedQuery.cc.js and trace reviveTransportedQueryRef, useBackgroundQuery, and ReadFromReadableStreamLink to understand how the transported stream and query references are created. Reproduce the Next.js App Router example, then verify that the child useSuspenseQuery reads the transported result without creating duplicate network requests.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.