TanStack / TanStack/query

Hydration error thrown when using `PersistQueryClientProvider` and `Suspense`

Open
#6,472 6 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

package: react-query
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 localStorage using PersistQueryClientProvider and createAsyncStoragePersister

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 status is always pending in the server, and a loader is returned
  • Client: The client hydrates, but it instantly has access to the cached user and 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.tsx file and notice no hydration errors thrown
  • Wrap the SomeData component in layout.tsx with a Suspense and 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.