Closed MoustaphaDev closed 2 years ago
A confirmation: Is this persist
only issue? Or, can the error be reproducible without persist
?
Thanks for the fast reply. It is in fact persist
only issue.
@AnatoleLucet can have a look please? (I'm still not sure if this is something that can be solved only with persist
.)
@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 useHydrated
hook trick solved it, thank you!
I'm having the same problem, can only reproduce with persist
- however, I'm using Typescript. Will try useHydrated
and post follow-up.
Follow-Up: Using the useHasHydrated
hook works, but checking for hydration everywhere isn't very DRY. There has to be a simpler way to achieve this globally. 🤔
@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. useHasHydrated
via a custom useEffect
), was to trick Zustand into skipping the initial render-cycle before getting the store from window.localStorage
.
@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 setTimeout
inside getItem
. A simple promise (or async
function) still wouldn't fix the issue.
Looks ugly, feels ugly… let's face it: "It's ugly"
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 (ignore the types if you don't need them)
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;
};
useHydratedStore
is only supposed to be used for getters in the case there is a hydration mismatch
const fishes = useHydratedStore('fishes');
It is used differently than useStore
is, in that you pass a string instead of a function, which isn't ideal, but it works for me for now as a fully type-safe solution.
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 router.isReady
from useRouter
to conditionnally display the component that is using the persisted store data? It “works for me” 😅
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: @.***>
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>
}
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 useStore
hook that takes in the store you require to access, and the callback e.g. (state) => state.count
All credit to the author, here is the proposed solution (with an added export for the useStore
function):
import { useEffect, useState } from 'react'
export const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F
const [data, setData] = useState<F>()
useEffect(() => {
setData(result)
}, [result])
return data
}
Call this function to access state like so:
const store = useStore(useAuthStore, (state) => state)
It works with individual state objects too, and UI updates occur as expected when the state changes:
const fishCount= useStore(useAuthStore, (state) => state.fishCount)
Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5
Here's an option using Nextjs' dynamic
to lazy-load the target component.
I used it in my project to persist some table filters in localstorage.
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 article is a nice solution to this problem.
The solution is to create your own
useStore
hook that takes in the store you require to access, and the callback e.g.(state) => state.count
All credit to the author, here is the proposed solution (with an added export for the
useStore
function):import { useEffect, useState } from 'react' export const useStore = <T, F>( store: (callback: (state: T) => unknown) => unknown, callback: (state: T) => F, ) => { const result = store(callback) as F const [data, setData] = useState<F>() useEffect(() => { setData(result) }, [result]) return data }
Call this function to access state like so:
const store = useStore(useAuthStore, (state) => state)
It works with individual state objects too, and UI updates occur as expected when the state changes:
const fishCount= useStore(useAuthStore, (state) => state.fishCount)
Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5
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 useStore
hook solution from @shanehoban I now also get another error.
If I use the store values in multiple components I get the following warning:
Warning: Cannot update a component (`Libraries`) while rendering a different component (`CookieBanner`).
To locate the bad setState() call inside `CookieBanner`, follow the stack trace as described in
https://reactjs.org/link/setstate-in-render
You can skip hydration and trigger it manually by calling store.persist.rehydrate()
. I do it on a useEffect
hook at _app.tsx
and got rid of the hydration error. Keep in mind this is SSR/SSG-friendly too.
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 store.type
and what this variable is to be fair. Would love to know the details tho.
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 persist
middleware, and (2) a client-side-only store which includes it.
I did this because using persist
during SSR/SSG yields the following warning log:
[zustand persist middleware] Unable to update item 'your-named-storage', the given storage is currently unavailable.
I use store.type === 'client'
to know if store.instance.persist
is defined and perform re-hydration. store.type
is a custom discriminator string I made to distinguish between both types of stores.
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 // rehydrate zustand store after next.js hydrates
.
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! store.instance
is specific to my solution, so don't mind it. However, the idea remains the same: skip zustand
re-hydration until next.js
hydrates by setting { skipHydration: true }
in your store config and calling <your-zustand-store>.persist.rehydrate()
inside a useEffect
callback. Code you write inside useEffect
is sure to run after next.js
hydrates.
I made some changes to my original solution; you can find the source code here, but I will explain the main beats of it.
zustand
storenext.js
can run both on the server and client, right?persist
middleware in my server-side store, only on the client-side one.createPokemonStore.onClient()
and createPokemonStore.onServer()
.preloadedState
object, which represents data populated by getStaticProps/getServerSideProps
.zustand
stores directly, I wrap them in a plain, old JavaScript object that comes attached with a unique discriminator string: the store type
.type
can have one of two values: 'client'
for client-side stores, and 'server'
for server-side stores.zustand
store instance in a property called instance
.// create a zustand store...
const getPokemonStore = {
// ...for the client
onClient: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
const fullStateCreator = createFullStateCreator(preloadedState);
return {
type: 'client' as const,
// (1) zustand store will be persisted into local storage so it doesn't dissappear
// after a page refresh, meaning we require the persist() middleware
// (2) { skipHydration: true } is required to avoid hydration errors,
// please visit https://github.com/pmndrs/zustand/issues/938 for more information on it
instance: create<PokemonState>()(
persist(fullStateCreator, {
name: 'pokemon-storage',
skipHydration: true,
})
),
};
},
// ...for the server: persist() middleware is not needed
onServer: (preloadedState?: Partial<PokemonStateWithoutFunctions>) => {
const fullStateCreator = createFullStateCreator(preloadedState);
return {
type: 'server' as const,
instance: create<PokemonState>()(fullStateCreator),
};
},
};
zustand
store: useStoreRef
Note: This is where things deviated a little bit from my original solution, but the main idea remains the same.
useStoreRef
is a hook that is assigned with the following taskszustand
store once on the client and once on the server using a ref
. Why a ref
? To avoid re-renders.zustand
store directly, I expose an API of sorts (a collection of callback functions) that will allow my components to interact with the zustand
store. Three callbacks compose the API: a (1) select
function to select data from the store, a (2) rehydrateFromStorage
function to rehydrate our zustand
store once, and (3) a rehydrate
callback to perform a manual re-hydration whenever the developer needs to.export interface UseStoreRefApi {
select: <T>(selector: (state: PokemonFullState) => T) => T;
storage: {
rehydrate: () => void;
};
rehydrate: (partialState: Partial<PokemonState>) => void;
}
// (1) hook that creates a zustand store instance and saves it into a ref
// (2) why a ref? to avoid triggering unnecessary re-renders
// (3) this hook is called in BOTH server-side and client-side
const useStoreRef = (
preloadedState?: Partial<PokemonStateWithoutFunctions>
): UseStoreRefApi => {
// get factory depending on context (server or client)
const createStore = IS_SERVER
? createPokemonStore.onServer
: createPokemonStore.onClient;
// (1) initialize server-side and first client-side store instance once
// with the same, pre-loaded state (to avoid hydration errors)
// (2) preloadedState comes from _app.tsx (pageProps.preloadedState)
// (3) refs are preferred as they don't trigger re-renders when mutated
const storeRef = useRef<PokemonStore>();
if (!storeRef.current) storeRef.current = createStore(preloadedState);
// one-shot flag to hydrate store from storage only once
const hasRehydratedFromStorage = useRef(false);
// api method 1: select a piece of state data from the store ref via a selector
const select = useCallback(
<T>(selector: (state: PokemonFullState) => T) =>
storeRef.current!.instance(selector),
[]
);
// (1) api method 2: hydrate client-side store with local-storage data
// (2) hydration must only be executed once, hence the hasRehydratedFromStorage flag
const rehydrateFromStorage = useCallback(() => {
if (
storeRef.current &&
storeRef.current.type === 'client' &&
!hasRehydratedFromStorage.current
) {
storeRef.current.instance.persist.rehydrate();
hasRehydratedFromStorage.current = true;
}
}, []);
// (1) api method 3: re-hydrate client-side store with some partial state data
// (2) this is useful to override current state data with page-specific data that
// was fetched either from getStaticProps/getServerSideProps or through a client-side
// external API request
const rehydrate = useCallback((partialState: Partial<PokemonState>) => {
if (storeRef.current)
storeRef.current.instance.getState().rehydrate({
...storeRef.current.instance.getState(),
...partialState,
});
}, []);
// this hook hides our zustand store and just exposes an api to interact with
return { select, storage: { rehydrate: rehydrateFromStorage }, rehydrate };
};
The
useRef
that contains ourzustand
store is of typePokemonStore
, which is a union type of both data types that could be returned by either callingcreatePokemonStore.onServer
orcreatePokemonStore.onServer
. Remember that this is all code that can run on both sides!
export type PokemonServerStore = ReturnType<typeof createPokemonStore.onServer>;
export type PokemonClientStore = ReturnType<typeof createPokemonStore.onClient>;
// zustand store type definition, which varies depending on the setting (client or server)
// store.type being 'client' or 'server' serves as a discriminator to distinguish between
// the two versions of this store
export type PokemonStore = PokemonServerStore | PokemonClientStore;
useStoreRef
at _app.tsx
zustand
store API from useStoreRef
and inject it into a PokemonContext
I had beforehand.pageProps.preloadedState
represents the data populated by our getStaticProps/getServerSideProps
functions we define in our page componentsexport default function App({ Component, pageProps }: AppProps) {
// get store api
const api = useStoreRef(pageProps.preloadedState);
// zustand context injected with store api: all next.js pages now have access to it
return (
<PokemonContext.Provider value={api}>
<Component {...pageProps} />
</PokemonContext.Provider>
);
}
PokemonContext
definition and a custom usePokemonStore
hook that simply checks the context is defined.// our zustand context, which simply holds what's returned by
// useStoreRef() at _app.tsx, which is an API to interact with
// the zustand store
export const PokemonContext = createContext<UseStoreRefApi | null>(null);
const usePokemonStore = () => {
const api = useContext(PokemonContext);
if (!api) throw new Error('context not found');
return api;
};
zustand
store with server-side datazustand
store.preloadedState
.// getServerSideProps can populate some zustand state data
export const getServerSideProps: GetServerSideProps<{
preloadedState: Partial<PokemonStateWithoutFunctions>;
}> = async () => {
const response = await fetch(
'http://jherr-pokemon.s3.us-west-1.amazonaws.com/index.json'
);
const pokemons = (await response.json()) as Pokemon[];
// create a short-lived, server-side-only zustand store
const pokemonStore = createPokemonStore.onServer().instance;
// set raw pokemon list on store
pokemonStore.getState().setPokemons(pokemons);
// get current state snapshot
const pokemonState = pokemonStore.getState();
// (1) populate props.preloadedState with server-side-fetched data
// (2) remember, this is grabbed by _app.tsx at pageProps.preloadedState
// and is used by useStoreRef() to create the both server and client zustand stores
// (3) the server zustand store is used for SSR/SSG generation
// (4) the client zustand store is used for app state management which comes
// with pre-loaded data from the server in this case
return {
props: {
preloadedState: {
pokemons: pokemonState.pokemons,
},
},
};
};
usePokemonStore
in our page components to select data and perform storage re-hydrationuseEffect
code of your page, call the rehydrateFromStorage
API callback to synchronize our zustand
store data with the storage system data. Remember that rehydrateFromStorage
realizes the actual <your-zustand-store>.persist.rehydrate()
call, and it will do it just once.interface HomeProps {
preloadedState: Partial<PokemonStateWithoutFunctions>;
}
export default function Home(props: HomeProps) {
const api = usePokemonStore();
const {
select,
storage: { rehydrate: rehydrateFromStorage },
rehydrate,
} = api;
// (1) access zustand store non-computed and computed properties with api.select()
// (2) api.select() runs in BOTH server and client
// (3) in server, api.select() grabs the server-side zustand store and is used
// for SSR/SSG generation
// (4) in client, api.select() grabs the client-side zustand store and is used
// for dynamic state management
const filteredPokemons = select((state) => state.filteredPokemons);
const filter = select((state) => state.filter);
const setFilter = select((state) => state.setFilter);
useEffect(() => {
// when page loads, rehydrate our store with local-storage data (in reality, this occurs once)
rehydrateFromStorage();
// (1) rehydrate our store with props.preloadedState, which
// is data provided by getServerSideProps/getStaticProps
// (2) instead of passing props directly, you can make client-side requests to external APIs
// which eventually lead to a state-like object that should override current state data
// (3) since in this case we are fetching our updated data in getServerSideProps, we can
// simply use props.preloadedState to perform this overriding
rehydrate(props.preloadedState);
}, [rehydrateFromStorage, rehydrate, props.preloadedState, removeMainClass]);
return (
<div className={mainClasses}>
<Head>
<title>Pokemon</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.png" />
</Head>
<div>
<Link href="/count">Count</Link>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
className={styles.search}
/>
</div>
<div className={styles.container}>
{filteredPokemons.slice(0, 20).map((pokemon) => (
<div key={pokemon.id} className={styles.image}>
<img
alt={pokemon.name}
src={`http://jherr-pokemon.s3.us-west-1.amazonaws.com/${pokemon.image}`}
/>
<h2>{pokemon.name}</h2>
</div>
))}
</div>
</div>
);
}
Let me know if you more questions about my solution; you may even come up with a much more elegant one. You may also download the repository I linked before to see it in action and without errors for yourself.
On that same repo I have branches that implement the same behavior with other state management systems (redux, mobx, and jotai), in case you are interested.
@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 null|undefined
values you need to set before hydration, killing any potential of SSR/SSG in those sections.
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 undefined
store issue and it's much simpler, although it may not work as well as his for all use cases.
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 undefined
before rendering data that depends on it.
...
// 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 app router
and I resolved it like this:
skipHyrdation: true
to persist function
export const useBoundStore = create(
persist(
() => ({
count: 0,
// ...
}),
{
// ...
skipHydration: true,
}
)
Then in my page.tsx I added this logic:
export default function Home() {
const {count} = useBoundStore ();
const [hasHydrated, setHasHydrated] = useState(false);
// Rehydrate the store on page load
useEffect(() => {
useBoundStore.persist.rehydrate();
setHasHydrated(true);
}, []);
if (!hasHydrated) return null;
return (
<div>{count}</div>
);
}
I also had a problem with this issue but in a
app router
and I resolved it like this:
- I added
skipHyrdation: true
to persist functionexport const useBoundStore = create( persist( () => ({ count: 0, // ... }), { // ... skipHydration: true, } )
- Then in my page.tsx I added this logic:
export default function Home() { const {count} = useBoundStore (); const [hasHydrated, setHasHydrated] = useState(false); // Rehydrate the store on page load useEffect(() => { useBoundStore.persist.rehydrate(); setHasHydrated(true); }, []); if (!hasHydrated) return null; return ( <div>{count}</div> ); }
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 rehydrate
like this: void useCartStore.persist.rehydrate();
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:
// store.js
const initialState = { ... }
let store = (set, get) => ({ ...initialState })
store = immer(store);
store = persist(store, {
name: 'dashboard-store',
partialize: (state) => ({
tokens: state.tokens
...
}),
storage: createJSONStorage(() => localStorage),
});
const useDashboardStore = create(store);
const useSSRDashboardStore = withSSR(useDashboardStore, initialState);
/* Atomic selectors */
/* Persisted */
/* Persisted store hooks return a tuple of [state, hasHydrated] so the result of the call has to be destructured */
export const useTokens = () => useSSRDashboardStore((state) => state.tokens);
The atomic selectors might use both the normal store or the SSR version. The SSR version is produced using this HOF:
// withSSR.js
/* HOF that allows to use a persisted store on SSR since it doesn't return directly the store value, but a duplicate react state valorized after the rendering on client has completed */
export const withSSR = (useStore, initialState) => (selector) => {
const [value, setValue] = useState(selector(initialState));
const [hasHydrated, setHasHydrated] = useState(false);
const hydratedValue = useStore(selector);
useEffect(() => {
startTransition(() => {
setValue(hydratedValue);
setHasHydrated(true);
});
}, [hydratedValue]);
return [value, hasHydrated];
};
Then in a React component, when subscribing to a part of the state that is persisted, you just need to remember to use destructuring:
// Tokens.js
...
const [tokens, hasHydratedTokens] = useTokens()
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 skipHydration: true
in your persist stores.
Then in your _app.tsx
or layout.tsx
use this hook and show a loader when hydrating the stores.
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... I haven't tried lazy loading the components but I guess that would be slow as well
@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 Thanks. This answer solved my problem really quickly ! 🙌
I don't get the initial value as undefined if I call hasHydrated e.g.
const router = useRouter();
const activeOrgId = useOrganisationStore((store) => store.activeOrgId);
if (!activeOrgId && useOrganisationStore.persist.hasHydrated()) {
router.replace("/playground/site/select");
}
return (
<div>
<div> layout</div>
{children}
</div>
);```
I also had a problem with this issue but in a
app router
and I resolved it like this:
- I added
skipHyrdation: true
to persist functionexport const useBoundStore = create( persist( () => ({ count: 0, // ... }), { // ... skipHydration: true, } )
- Then in my page.tsx I added this logic:
export default function Home() { const {count} = useBoundStore (); const [hasHydrated, setHasHydrated] = useState(false); // Rehydrate the store on page load useEffect(() => { useBoundStore.persist.rehydrate(); setHasHydrated(true); }, []); if (!hasHydrated) return null; return ( <div>{count}</div> ); }
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 page.tsx
file
const [isLoading, setLoading] = React.useState(true)
React.useEffect(() => {
setTimeout(() => {
setLoading(false)
}, 1000)
}, [])
if (isLoading) {
return (
<LoadingScreen />
)
}
return (
...
)
You can address the issue using dynamic
imports to disable SSR
for the specific component that relies on the Zustand store. Here's how you I made it:
import dynamic from 'next/dynamic';
import { useBearStore } from './store';
const Component = () => {
const bears = useBearStore((state) => state.bears);
return <div>{bears}</div>;
};
// Disable SSR for this component
export default dynamic(() => Promise.resolve(Component), { ssr: false });
This article is a nice solution to this problem.
The solution is to create your own
useStore
hook that takes in the store you require to access, and the callback e.g.(state) => state.count
All credit to the author, here is the proposed solution (with an added export for the
useStore
function):import { useEffect, useState } from 'react' export const useStore = <T, F>( store: (callback: (state: T) => unknown) => unknown, callback: (state: T) => F, ) => { const result = store(callback) as F const [data, setData] = useState<F>() useEffect(() => { setData(result) }, [result]) return data }
Call this function to access state like so:
const store = useStore(useAuthStore, (state) => state)
It works with individual state objects too, and UI updates occur as expected when the state changes:
const fishCount= useStore(useAuthStore, (state) => state.fishCount)
Link to article: https://dev.to/abdulsamad/how-to-use-zustands-persist-middleware-in-nextjs-4lb5
I faced an issue when using this
From from the docs,
// useStore.ts
import { useState, useEffect } from 'react'
const useStore = <T, F>(
store: (callback: (state: T) => unknown) => unknown,
callback: (state: T) => F,
) => {
const result = store(callback) as F
const [data, setData] = useState<F>()
useEffect(() => {
setData(result)
}, [result])
return data
}
export default useStore
setData(result)
should be setData(() => result)
Because if result is a function, it will be called by setState
and this will cause unexpected behaviour.
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?