apollographql / apollographql/apollo-client-integrations

PreloadQuery-transported boundary re-suspends during hydration recovery despite fully cached data

Open
#568 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

### Summary

When React discards server HTML and client-renders during hydration (e.g. recovering from an incidental hydration mismatch, minified React error #418), a `PreloadQuery`-transported Suspense boundary **re-suspends and commits its fallback**, even though the transported query result is already **fully present in the Apollo cache** at that exact moment.

User-visible effect: content that was streamed and already visible on screen is replaced by the loading skeleton for ~300 ms, then reappears. Any incidental mismatch anywhere on the page (env-dependent text, browser extensions, Google Translate, third-party scripts) turns into a full skeleton flash of every transported boundary on the page.

### Reproduction

**https://github.com/Oskii2311/apollo-suspense-hydration-repro**

```sh
pnpm install
pnpm build
pnpm start # port 3000
node harness/measure.mjs # measures /bug and /control
```

- `/bug` - deliberate hydration mismatch above a `PreloadQuery > Suspense > useSuspenseQuery` boundary (stand-in for any real-world incidental mismatch) → **reproduces**
- `/control` - identical page without the mismatch → clean

The harness delays JS chunks by 2 s so the streamed HTML (including the transported data) fully arrives before hydration starts - equivalent to a fast network + slow CPU client, reproducible manually with DevTools CPU throttling.

- Browser-initiated GraphQL requests during the whole run: **0** - the boundary suspends on the transported queryRef, it does not refetch.
- The fallback duration is a constant ~300 ms regardless of the mock response delay (tested 500 ms vs 1500 ms) - consistent with React's fallback throttle, i.e. the underlying suspension is near-instant but still commits a fallback.

### Expected behavior

On a client re-render during hydration recovery, `useSuspenseQuery` reads the fully transported result synchronously from the cache - the recovery is invisible, no fallback is committed, as React's mismatch recovery is designed to be lossless.

### Actual behavior

The boundary suspends again and commits its fallback; already-visible content is replaced by the skeleton for ~300 ms.

### Versions

- `next` 16.3.0, `react` / `react-dom` 19.2.8
- `@apollo/client` 4.2.10
- `@apollo/client-integration-nextjs` 0.14.5 (latest)

### Real-world impact

In our production e-commerce app, every transported boundary (category product grids, PDP recommendation carousels) visibly reverted from rendered content to skeletons on mid-range mobile devices whenever anything discarded server HTML during hydration.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the linked reproduction and run pnpm install, pnpm build, pnpm start, then node harness/measure.mjs to compare /bug and /control. Trace the PreloadQuery > Suspense > useSuspenseQuery path during hydration recovery, using the reported transported cache state as the baseline. Done means the recovered boundary retains its rendered content without committing the fallback while the control case remains clean.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, next.js, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.