In this article, we'll discuss the common error that arises when using Zustand's persist middleware with Next.js. You might have received errors like "Text content does not match server-rendered HTML", "Hydration failed because the initial UI does not match what was rendered on the server" and "There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering". So if you're a Next.js developer using Zustand and facing similar issues, keep reading to learn how to use the persist middleware and solve the hydration error.
What is Zustand?
A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.
I'll assume you are familiar with TypeScript, React, Next.js, and Zustand. But first, let's create a Zustand store to get started.
Creating a store with persist middleware
import { create } from "zustand";
import { persist } from "zustand/middleware";
// Custom types for theme
import { User } from "./types";
interface AuthState {
isAuthenticated: boolean;
user: null | User;
token: null | string;
login: (email: string, password: string) => Promise<void>;
register: (userInfo: FormData) => Promise<void>;
logout: () => void;
}
const useAuthStore = create<AuthState>()(
persist(
(set) => ({
isAuthenticated: false,
user: null,
token: null,
login: async (email, password) => {
// Login user code
},
register: async (userInfo) => {
// Registering user code
},
logout: () => {
// Logout user code
},
}),
{
name: "auth",
}
)
);
export default useAuthStore;
The Problem
If we try to access the above store directly in the component, we'll get hydration errors if the user is logged in because it doesn't match the initial state of the store.
We will get hydration errors because Zustand contains data from persist middleware (Local Storage, etc...) while the hydration is not complete and the server-rendered store has initial state values, resulting in a mismatch in the store's state data.
Solution
I solved this problem by creating a state, writing the Zustand store's state values in it inside the useEffect hook, and then use that state to access the store values. Doing this on every component everywhere we access this store is tedious, inefficient, and messy, so I decided to write a custom hook for it. I also used TypeScript generics to maintain type safety.
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;
};
You have to access the store from this hook in your components.
const store = useStore(useAuthStore, (state) => state)
I was made aware of a similar strategy used in a YouTube video by someone. Although I found the solution on my own, I improved the types after seeing the video.
Conclusion
We can solve this hydration problem by creating a new state and updating that state with the store's state after receiving it from the server in the side effects. Furthermore, we can create a custom hook to reuse the logic and increase the modularity of our code. I hope that this post has helped you resolve the hydration error in your Next.js app or website. Happy Coding 🚀
Top comments (13)
How would you handle this case?
Because now the
useStore
returns undefined on first render.Also, even if we use four individual
useStore
s for every variable above, all of the values are undefined. Which means we have to add a ton of checks in JSX, right?Yes, I believe it would be beneficial to develop a Higher-Order Component (HOC) for performing checks or invoke the
useStore
function in the parent component with a single check and pass data as props in your case. We are redefining the store values after hydration, so we will have anundefined
state initially. Let me know if you found any better way to tackle this problem.Hi
Did you find a solution to the same? I am also getting undefined on the first render. Adding checks on all the pages will be too tedious
@keshav263 We added checks in our app. Fortunately, we had only two places where we had to modify the JSX and add the if statements. We didn't have to modify the entire app because the other components are rendered "later" in the rendering lifecyle due to us having loading screens. So by the time the loading screens disappear, the store has the values in it
Question: how did you add your checks? I'm struggling to do it once functions are involved, too
@nobilelucifero We ended up creating this hook and used it two-three times in the app:
In the code, it can be used like this:
The check is just that if statement in the JSX
I am not recommending to used this too many times because it makes the JSX full of ternary operators like that. The reason we use it just three times is because we have a loading screen which waits for the backend data to load. In that time, the currency is already loaded and we don't need to do that ternary checks. We only have three places that don't have a loading section and that's why we used this custom hook three times.
ahhh I see! It was simpler than what I was trying to (check to hydration and whether it was a variable or function). Amazing, thank you so so so much. I hope in a more proper solution soon too.
Awesome, this is the kind of solution I was looking for, linked it in this issue too: github.com/pmndrs/zustand/issues/938
Thanks!
I'm glad it helped!
or maybe you can create a check for hydration render a loading.. while your ui do not get hydrated
Yes. I think that should work as well.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.