-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wait for Nextjs rehydration before zustand store rehydration #938
Comments
A confirmation: Is this |
Thanks for the fast reply. It is in fact |
@AnatoleLucet can have a look please? (I'm still not sure if this is something that can be solved only with @MoustaphaDev Can the issue be reproducible with zustand v3.7.x? |
Yes it can, just tried to downgrade to v3.7.x but same error. |
I'm unable to reproduce. @MoustaphaDev could you send a repro in a codesandbox? |
Sure, here's the sandbox @AnatoleLucet. |
@MoustaphaDev Thanks. I think this is a dup of #324 |
Alright, your |
I'm having the same problem, can only reproduce with Follow-Up: Using the |
@ecam900 another option is to make your storage engine async (see this comment) which might better fit your use case. |
None of the options above or from the linked issues worked for us. The only way for us to remove the warning without checking if the component was mounted (e.g. @AnatoleLucet If you are sure that your solution works, it would be nice if you could provide an MVP Sandbox. I will provide a few Sandboxes with all solutions we've tested tomorrow, to allow better reproduction and show that my "workaround" works (while not optimal). This is the solution that works as a workaround for us: const useStore = create(
persist(
(set) => ({
counter: 0,
setCounter(counter) {
set({ counter });
},
}),
{
name: "next-zustand",
getStorage: () => ({
setItem: (...args) => window.localStorage.setItem(...args),
removeItem: (...args) => window.localStorage.removeItem(...args),
getItem: async (...args) =>
new Promise((resolve) => {
if (typeof window === "undefined") {
resolve(null);
} else {
setTimeout(() => {
resolve(window.localStorage.getItem(...args));
}, 0);
}
}),
}),
}
)
); The trick was to add a
getItem: async (key) =>
new Promise((resolve) => {
setTimeout(() => {
if (typeof window === "undefined") {
resolve(null);
} else {
setTimeout(() => {
resolve(window.localStorage.getItem(...args));
}, 0);
}
}),
}), Side-node: This issue started with React v18 since they seem to handle or warn about hydration different. This might be part of Next.js v12 or React.js v18, We are currently unsure about this. The only "other" option that worked for us was to not render until the component was mounted on the client but that means either of the following caveats
|
@pixelass if your app using authentication, you can send an cookies/local storage from persist middleware to server / next page with gerServersideProps. /**
* @callback getServersideProps
* @param {import('next').GetServerSidePropsContext} context
*/
/**
* @param {getServersideProps} getServersideProps
*/
const withStore = (getServersideProps = /*default*/ () => ({props:{}}) ) =>
/*return function*/ async context=>{
const {req} = context
const hasToken = req.cookies.token ? true : false
if (!hasToken) return {redirect: {destination: '/login', permanent: false} }
//get state from local storage / cookies
context.state = state =>req.cookies?.[state] ? JSON.parse(req.cookies[state]).state : null
return getServersideProps(context)
}
export const getServerSideProps = withStore(context => {
return {
props:{
myStore: context.state('myStorePersistName')
}
}
}
)
import {useMemo} from 'react'
import useStore from '../myStore'
export default function MyPage({myStore}) {
useMemo(() => myStore && useStore.setState(myStore),[])
const store = useStore(state => state.data)
return <div>{store}</div>
} |
I've been using a custom hook to wrap my persisted store type GetFunctionKeys<T> = {
[K in keyof T]: T[K] extends ((...args: any[]) => void) ? K : never;
}[keyof T];
type OmittedFunctionKeys<T> = Omit<T, GetFunctionKeys<T>>;
type StoreState = {
fishes: number,
addAFish: () => void,
};
const initialStates = {
fishes: 0,
};
const usePersistedStore = create<StoreState>()(
persist(
(set, get) => ({
fishes: initialStates.fishes,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage',
getStorage: () => localStorage,
},
),
);
const useHydratedStore = <T extends keyof OmittedFunctionKeys<StoreState>>(key: T)
: OmittedFunctionKeys<StoreState>[T] => {
const [state, setState] = useState(initialStates[key]);
const zustandState = usePersistedStore((persistedState) => persistedState[key]);
useEffect(() => {
setState(zustandState);
}, [zustandState]);
return state;
};
const fishes = useHydratedStore('fishes'); It is used differently than |
Another simple workaround: function component() {
const bears = useStore(state => state.bears)
const [loaded, setLoaded] = useState(false)
useEffect(() => {
setLoaded(true);
}, [bears]);
return (
<div>{loaded ? bears : ""}</div>
)
} |
Thanks @ShahriarKh, your workaround worked for me :) |
@KGDavidson wrap around with your solution, I do a selector version to bring back as normally use. So now, you can use the bound store without set the useEffect whenever you need the persist item from the store import { useEffect, useState } from 'react'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface States {
count: number
flag: boolean
}
interface Actions {
increment: () => void
decrement: () => void
toggleFlag: () => void
}
interface Store extends States, Actions {}
const initialStates: States = {
count: 0,
flag: false,
}
export const useSetBoundStore = create<Store>()(
persist(
(set, get) => ({
...initialStates,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
toggleFlag: () => set((state) => ({ flag: !state.flag })),
}),
{
name: 'persist-store',
}
)
)
export const useBoundStore = <T extends keyof States>(selector: (state: States) => States[T]): States[T] => {
const [state, setState] = useState(selector(initialStates))
const zustandState = useSetBoundStore((persistedState) => selector(persistedState))
useEffect(() => {
setState(zustandState)
}, [zustandState])
return state
} And in a NextJS file using it like this.. 'use client'
import { useBoundStore, useSetBoundStore } from '@/store/use-bound-store'
export default function PokeBallCard() {
const increment = useSetBoundStore((state) => state.increment)
const switchPokeball = useSetBoundStore((state) => state.toggleFlag)
const count = useBoundStore((state) => state.count)
const flag = useBoundStore((state) => state.flag)
return (
<>
<button type="button" onClick={increment}>
Click me my pokemon
</button>
<button type="button" onClick={switchPokeball}>
Toggle catcher
</button>
<div>{count}</div>
<div>{flag ? 'yes' : 'no'}</div>
</>
)
} |
What about using |
Hasn’t try it yet, do you have a example on that?
…On Mar 2, 2023 at 2:30 PM +0700, Alex S ***@***.***>, wrote:
What about using router.isReady from useRouter? It “works for me” 😅
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you commented.Message ID: ***@***.***>
|
Yes! This is what I extracted from my code: export default function MyComponent() {
const router = useRouter();
const { name } = useZustandState();
return <div>{router.isReady && <p>{name}</p>}</div>
} |
Oh I see, actually that going to be the similar logic though, however I do think that implementing with useEffect will be a little bit lighter and can use in normal react app too. Thank for an example! |
This article is a nice solution to this problem. The solution is to create your own All credit to the author, here is the proposed solution (with an added export for the
Call this function to access state like so:
It works with individual state objects too, and UI updates occur as expected when the state changes:
Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5 |
Here's an option using Nextjs' const LazyTable = dynamic(
() => import("@/components/Table"), // component using persisted store
{
ssr: false,
},
);
const MyTable = () => {
// ...
return (
<Flex flexDir="column" alignItems="center" m="1rem">
<LazyTable
columns={columns}
data={profitsQuery.data?.items ?? []}
/>
</Flex>
);
}
export default MyTable; |
This is a very nice solution, but it introduces a new error, namely it's now undefined on first render just like the comments in that article, how would we tackle this without fixing this each time in an ugly way in each component? |
Using the provided
|
You can skip hydration and trigger it manually by calling export default function App({ Component, pageProps }: AppProps) {
// your zustand store
const store = useStoreRef(pageProps.preloadedState);
useEffect(() => {
// rehydrate zustand store after next.js hydrates
store.type === 'client' && store.instance.persist.rehydrate();
}, []);
return (
<PokemonContext.Provider value={store}>
<Component {...pageProps} />
</PokemonContext.Provider>
);
} |
Above solutions work (with the caveat of undefined values on the first render). Nonetheless, I think that most of them seem to be a little bloated imho. @AlejandroRodarte proposed a nice solution, I don't get the need to check for That's my take using context + next app dir. /store/form export const createFormStore = () =>
createStore<FormStore>()(
persist(
(set, get) =>
({
// ...state here
} as FormStore),
{
name: "form-store",
skipHydration: true,
}
)
);
export const formStore = createFormStore();
export const FormContext = createContext<ReturnType<
typeof createFormStore
> | null>(formStore);
// Use this function to get the state in your app (pardon the any 🥺)
export function useFormStore(selector: (state: FormStore) => any) {
return useStore(useContext(FormContext)!, selector);
} /app/providers type Props = {
children: React.ReactNode;
};
export function Providers({ children }: Props) {
const formStoreRef = useRef(formStore);
useEffect(() => {
formStore.persist.rehydrate();
}, []);
return (
<FormContext.Provider value={formStoreRef.current}>
<Toaster />
<ApolloProvider client={client}>{children}</ApolloProvider>
</FormContext.Provider>
);
} Somewhere in your layout: // ...
<Providers>
<main className="flex flex-col items-start max-w-6xl min-h-screen mx-auto gap-y-12">
<OverlayHub />
{children}
</main>
</Providers>
// ... |
@7sne I apologize for not providing more context to the code I posted. My next.js app generates two types of stores: (1) a server-side-only store which does not include the I did this because using
I use const createPokemonStore = {
onClient: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
const fullStateCreator = createFullStateCreator(preloadedState);
return {
type: 'client' as const,
instance: create<PokemonState>()(
persist(fullStateCreator, {
name: 'pokemon-storage',
skipHydration: true,
})
),
};
},
onServer: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
const fullStateCreator = createFullStateCreator(preloadedState);
return {
type: 'server' as const,
instance: create<PokemonState>()(fullStateCreator),
};
},
}; export type PokemonStore = ReturnType<
typeof getPokemonStore.onClient | typeof getPokemonStore.onServer
>; |
I've tried some of these solutions but not all of them because they seem hard and I'm lazy. I guess I'll just have to rework my whole store and see if I can get it to work. I'd like to use store.persist.rehydrate() but it doesn't seem to do anything! I notice @AlejandroRodarte has the extra word "instance" in his call and maybe that has something to do with It'd be nice if there were a straightforward way to do this because as it stands it would seem that persist just doesn't really support Nextjs. |
@noconsulate Skipping hydration definitely works! I made some changes to my original solution; you can find the source code here, but I will explain the main beats of it. Creating the
|
@AlejandroRodarte Wow thanks for the detailed writeup. I hope many people see this! I have two stores, one of them works the way you describe here so I can try rehydrate() on that one. I had given up on persist and just threw in a 'beforeunload' event listener to prevent refresh but I think it's worth making this work. |
@noconsulate No problem man! Glad you liked it. I would say it's worth it. With this, you can have global state management without losing SSR/SSG perks. The reason I didn't like the solutions above was not the complexity, but the initial If you have available the source code where your issue is, feel free to share it so I can help you in more detail. |
It looks like @AlejandroRodarte did a lot of working figuring out a solution that works well, and I applaud him for it. I just wanted to chime in with what's currently working for me to handle the initial I just wanted to mention it here to to offer an alternative and also get others' feedback in case they see pitfalls with it that I haven't considered. It's implemented starting with the official guide's instructions, and does a check for the store to not be ...
// yourComponent.tsx
import useStore from './useStore'
import { useBearStore } from './stores/useBearStore'
export default function YourParentComponent() {
const bearStore = useStore(useBearStore, (state) => state);
// `store` check is required because this is server-rendered by Next.js
if (!bearStore) return null;
return (
<ChildComponent bearStore={bearStore} />
);
} |
I also had a problem with this issue but in a
|
Simple and effective. Might not be the most robust solution but definitely works as a quick workaround. I'll just add that if using TypeScript, you might need to call |
Hi, by reading several solutions proposed by the users and the official Zustand docs, I came up with this solution to handle in a clean way the SSR mismatch issue when using Zustand+NextJS:
The atomic selectors might use both the normal store or the SSR version. The SSR version is produced using this HOF:
Then in a React component, when subscribing to a part of the state that is persisted, you just need to remember to use destructuring:
Other methods used to check the hydration didn't work for me in NextJS since Zustand hydrates before NextJS. |
Im using this approach to manually hydrate multiple stores. import React from 'react';
import { Mutate, StoreApi } from 'zustand';
export const useHydrateStores = <S extends Array<Mutate<StoreApi<any>, [['zustand/persist', any]]>>>(...stores: S) => {
const [hasHydrated, setHasHydrated] = React.useState(stores.length === 0);
React.useEffect(() => {
if (!hasHydrated && stores.length > 0) {
let promises: Promise<boolean>[] = [];
const subscriptions: (() => void)[] = [];
stores.forEach(store => {
promises.push(
new Promise(r => {
subscriptions.push(store.persist.onFinishHydration(() => r(true)));
}),
);
store.persist.rehydrate();
});
Promise.all(promises).then(res => setHasHydrated(res.every(Boolean)));
return () => {
promises = [];
subscriptions.forEach(unsub => unsub());
};
}
}, [hasHydrated, stores]);
return hasHydrated;
}; You must set const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const hasHydrated = useHydrateStores(useStoreOne, useStoreTwo, ...moreStores);
if (!hasHydrated) {
return <PageLoader />;
}
const getLayout = Component.getLayout || (page => page);
return (
<RootLayout title="My Site">
{getLayout(<Component {...pageProps} />)}
</RootLayout>
);
}; |
I have tried the solution with the useEffect, however it seems really slow, I get whole seconds of the rest of the UI being shown and then the useEffect kicks in and adds the data which finally render the associated component. However if I just load the data straight from the zustand store without useEffect, I get the component to render right away correctly, the only thorn here are the hydration errors... |
@netgfx if both server and client doesn't render the same thing on the first render then you face hydration errors, you can avoid that having a loading state |
@ShahriarKh |
I don't get the initial value as undefined if I call hasHydrated e.g.
|
Thank you for this. From your example, we can show a loading screen for 1-2 seconds and then show the page content, instead of return null. It worked. Something just like this in the const [isLoading, setLoading] = React.useState(true)
React.useEffect(() => {
setTimeout(() => {
setLoading(false)
}, 1000)
}, [])
if (isLoading) {
return (
<LoadingScreen />
)
}
return (
...
) |
You can address the issue using
|
I faced an issue when using this From from the docs,
Because if result is a function, it will be called by |
Hello, I'm using the
persist
middleware on my store with Nextjs (SSG) and I got several warnings in dev mode all pointing:Error: Hydration failed because the initial UI does not match what was rendered on the server.
It doesn't break the app however.
Looks like the zustand store rehydrates before Nextjs has finished its rehydration process.
Is there a way trigger the rehydration of the zustand store after Nextjs' rehydration process or are there better ways of handling this?
The text was updated successfully, but these errors were encountered: