DEV Community

Cover image for How to use Zustand's persist middleware in Next.js
Abdul Samad Ansari
Abdul Samad Ansari

Posted on • Updated on • Originally published at blog.abdulsamad.dev

How to use Zustand's persist middleware in Next.js

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;
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

You have to access the store from this hook in your components.

const store = useStore(useAuthStore, (state) => state)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
ionellupu profile image
Ionel Cristian Lupu

How would you handle this case?

const [channelId, channelInfoLoaded, currencyCode, countryCode] = useStore(myStore, state => [
        state.channelId,
        state.channelInfoLoaded,
        state.currencyCode,
        state.countryCode,
    ])
Enter fullscreen mode Exit fullscreen mode

Because now the useStore returns undefined on first render.

Also, even if we use four individual useStores for every variable above, all of the values are undefined. Which means we have to add a ton of checks in JSX, right?

Collapse
 
abdulsamad profile image
Abdul Samad Ansari

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 an undefined state initially. Let me know if you found any better way to tackle this problem.

Collapse
 
keshav263 profile image
Keshav Chhaparia

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

Collapse
 
ionellupu profile image
Ionel Cristian Lupu

@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

Thread Thread
 
nobilelucifero profile image
Lucifero Von Nachtosphere

Question: how did you add your checks? I'm struggling to do it once functions are involved, too

Thread Thread
 
ionellupu profile image
Ionel Cristian Lupu

@nobilelucifero We ended up creating this hook and used it two-three times in the app:

/**
 * Use this function when you want to fix the "hydration" error in NextJS
 * @param store
 * @param callback
 */
export const useAsyncStore = <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
}
Enter fullscreen mode Exit fullscreen mode

In the code, it can be used like this:

const currencyCode = useAsyncStore(useAppContext, state => state.currencyCode)

return <>
   <div>... some other JSX you have here</div>
   {currencyCode? <TheComponentThatRequiresCurrencyCode currency={currencyCode}/>: undefined}
</>
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
nobilelucifero profile image
Lucifero Von Nachtosphere

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.

Collapse
 
guillaumeduhan profile image
CodewithGuillaume

Mate, this is by far the most efficient article I've read! Thank you so much!

Collapse
 
abdulsamad profile image
Abdul Samad Ansari

You're very welcome! I'm glad it helped.

Collapse
 
shanehoban profile image
Shane Hoban

Awesome, this is the kind of solution I was looking for, linked it in this issue too: github.com/pmndrs/zustand/issues/938

Thanks!

Collapse
 
abdulsamad profile image
Abdul Samad Ansari

I'm glad it helped!

Collapse
 
nyctonio profile image
Ritesh Kumar

or maybe you can create a check for hydration render a loading.. while your ui do not get hydrated

Collapse
 
abdulsamad profile image
Abdul Samad Ansari

Yes. I think that should work as well.