zustand icon indicating copy to clipboard operation
zustand copied to clipboard

Zustand store in React context with Suspense causes server-client mismatch

Open max-ch9i opened this issue 2 years ago • 11 comments

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:

  1. On the server, <Navigation> renders with theme system
  2. On the client, <PageComponents> renders first and in useEffect updates theme to dark
  3. Then, after a delay in <Suspense>, <NavigationProvider> renders <Navigation>, which receives theme dark
  4. 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.

max-ch9i avatar Dec 07 '22 13:12 max-ch9i

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.

dai-shi avatar Dec 08 '22 23:12 dai-shi

Before raising this issue, I set up a reduced example with context only but without zustand and it worked as expected.

max-ch9i avatar Dec 09 '22 09:12 max-ch9i

@dai-shi Here's an example without zustand: https://stackblitz.com/edit/nextjs-b6vatg?file=app%2FContextProvider.js

max-ch9i avatar Dec 09 '22 09:12 max-ch9i

Hmm, maybe we need to use useSyncExternalStore to reproduce it..

dai-shi avatar Dec 09 '22 11:12 dai-shi

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

max-ch9i avatar Dec 09 '22 13:12 max-ch9i

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.

max-ch9i avatar Dec 09 '22 14:12 max-ch9i

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.

dai-shi avatar Dec 10 '22 00:12 dai-shi

No, it doesn't. https://stackblitz.com/edit/nextjs-98e2ub?file=app%2FContextProvider.js

dai-shi avatar Dec 10 '22 00:12 dai-shi

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.

max-ch9i avatar Dec 10 '22 13:12 max-ch9i

Ah, nice catch.

Then, I think the issue can be reproduced with useSyncExternalStore without zustand.

dai-shi avatar Dec 10 '22 13:12 dai-shi

Created an issue for next: https://github.com/vercel/next.js/issues/43920

max-ch9i avatar Dec 10 '22 13:12 max-ch9i

perhaps this video would be helpful: https://youtu.be/OpMAH2hzKi8

JacobWeisenburger avatar Mar 29 '23 16:03 JacobWeisenburger

@JacobWeisenburger beware of this

Screenshot_20230402-224903.png

OlegLustenko avatar Apr 02 '23 19:04 OlegLustenko