zustand
zustand copied to clipboard
Zustand store in React context with Suspense causes server-client mismatch
Summary
A combination of suspense, client context and zustand causes a server-client mismatch error. If suspense is removed, the behaviour is as expected.
The app structure overview:
<ContextProvider>: client component
- <Suspense>
-- <NavigationProvider>: server component
--- <Navigation>: client component
- <PageComponents>: client component
My guess as to what is happening:
- On the server,
<Navigation>
renders with themesystem
- On the client,
<PageComponents>
renders first and inuseEffect
updates theme todark
- Then, after a delay in
<Suspense>
,<NavigationProvider>
renders<Navigation>
, which receives themedark
- Mismatch error
Link to reproduction
https://stackblitz.com/edit/nextjs-ydmeqq?file=app%2Flayout.js
Check List
Please do not ask questions in issues.
- [x] I understand this is not a question.
My guess as to what is happening:
In such a case, I don't think there's anything zustand can do.
Can you reproduce the issue without zustand? With useState
or maybe with useRef
. I think you can open an issue in nextjs repo.
Let's keep this issue open for a while.
Before raising this issue, I set up a reduced example with context only but without zustand and it worked as expected.
@dai-shi Here's an example without zustand: https://stackblitz.com/edit/nextjs-b6vatg?file=app%2FContextProvider.js
Hmm, maybe we need to use useSyncExternalStore
to reproduce it..
Page https://beta.reactjs.org/apis/react/useSyncExternalStore has a note, saying that:
Make sure that getServerSnapshot returns the same exact data on the initial client render as it returned on the server. For example, if getServerSnapshot returned some prepopulated store content on the server, you need to transfer this content to the client. One common way to do this is to emit a
In useStore
, the value for getServerSnapshot
always falls back to getState
:
api.subscribe,
api.getState,
api.getServerState || api.getState,
selector,
equalityFn
)
and getState
on the first render in the example returns a value for theme
that is not the the value rendered on the server.
A solution seems to be that zustand must serialise the server store state into a JS object, emit it in a
I've created an absolute minimal example that replicates the error and does not involve context or suspense: https://stackblitz.com/edit/nextjs-euctcq?file=app%2Fpage.js
Basically, if there's anything that modifies the store state before any of the components renders using a value from the context, the error will happen.
Changing store is invalid, and useEffect
is required.
https://stackblitz.com/edit/nextjs-d1oqcx?file=app%2Fpage.js
I guess, you need suspense to reproduce the issue.
And, yeah, getServerState
can be a solution.
No, it doesn't. https://stackblitz.com/edit/nextjs-98e2ub?file=app%2FContextProvider.js
Updated your example to use a vanilla store. https://stackblitz.com/edit/nextjs-f1ityy?file=app%2FContextProvider.js
Before getServerState did not get assigned to api
but to the hook.
Now theme
renders with the correct value the first time, but surprisingly the error is still there.
Ah, nice catch.
Then, I think the issue can be reproduced with useSyncExternalStore without zustand.
Created an issue for next: https://github.com/vercel/next.js/issues/43920
perhaps this video would be helpful: https://youtu.be/OpMAH2hzKi8
@JacobWeisenburger beware of this