Hydration error thrown when using `PersistQueryClientProvider` and `Suspense`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 50.3k
- Forks
- 4.2k
- Avg merge
- 18h 25m
- Merged PRs (30d)
- 200
Description
Describe the bug
My real use-case:
- A modern Next.js app with authentication
- The authentication is completely offloaded to the browser
- The server does not have an auth token (a JWT, but knows if the user is potentially logged in if a refresh token is present)
- When logged in, the server returns loaders for the UI parts that are user specific
- The browser replaces them with actual data
- The browser caches data in
localStorageusingPersistQueryClientProviderandcreateAsyncStoragePersister
However, the example attached isn't that complex, it's a minimal example which reproduces the issue.
For the case above, the browser might have instant access to cached authenticated data. For instance, let's say we want to print the user's email address. The user has logged in earlier and this data is cached in localStorage, and as mentioned above, the server doesn't have this data. This is what happens:
- Both the server and the client call the same
useQuery. - Server: Since this data is not prefetched, the
statusis alwayspendingin the server, and a loader is returned - Client: The client hydrates, but it instantly has access to the cached
userand prints the email address
There is a mismatch here since the server rendered loading, while the client the email address. This case seems to be handled well when a component is not in a Suspense, but when it is, the last step will throw hydration errors (whether that's a loading.tsx or just a manual <Suspense>).
I have added some logs, and here is what I get:
Component not in Suspense:
not in suspense {status: 'pending', data: undefined, isRestoring: true}
not in suspense {status: 'success', data: {…}, isRestoring: false}
Component in Suspense:
in suspense {status: 'success', data: {…}, isRestoring: true}
in suspense {status: 'success', data: {…}, isRestoring: false}
When the component within a Suspense is rendering for the first time, the status is success, which renders data differently from the server, which I believe is the cause of the hydration error.
From the repo, here is part of the code that handles the loading UI:
const { data, status } = useSomeData();
if (status === "pending") {
return <main>loading</main>;
}
The workaround is:
const { data, status } = useSomeData();
const isRestoring = useIsRestoring();
if (status === "pending" || isRestoring) {
return <main>loading</main>;
}
That works, and does not throw hydration errors. But I feel that is bad DX, and I would prefer not to have to do that for every query call.
Your minimal, reproducible example
https://github.com/ali-idrizi/react-query-suspense-hydration-error
Steps to reproduce
Unfortunately it's a bit difficult to test this on CodeSandbox, as it doesn't seems to throw any hydration errors for some reason. Even rendering a simple {typeof window === 'undefined' ? 'x' : 'y'} does not throw an error for me. I have attached a repo instead, and you might have to run this locally, but there is also a video recording below.
To see the error, you will have to load the page once, so the data gets cached in the browser, afterwards the issue occurs on every subsequent reload.
You can try:
- Remove the
loading.tsxfile and notice no hydration errors thrown - Wrap the
SomeDatacomponent inlayout.tsxwith aSuspenseand notice the hydration error count increase
Expected behavior
Perhaps keep status as pending while isRestoring === true, similar to when a component is not within a Suspense?
How often does this bug happen?
Every time
Screenshots or Videos
https://github.com/TanStack/query/assets/20397725/4581ed80-60b1-4b36-b2b3-d16136f25bc1
Platform
- Windows 10 (WSL)
- Chrome 119
Tanstack Query adapter
react-query
TanStack Query version
5.12.2
TypeScript version
5.2.2
Additional context
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the linked minimal reproduction locally, then compare the behavior with and without loading.tsx and with SomeData wrapped in Suspense in layout.tsx. Trace PersistQueryClientProvider, createAsyncStoragePersister, and useIsRestoring to determine the expected pending state during restoration; done means cached data no longer causes a hydration mismatch in the reproduction.
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
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100