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

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

Solving the hydration error occurring with Zustand's persist middleware in Next.js

ยท

3 min read

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 ๐Ÿš€

ย