Fix Next.js 14 hydration error with Zustand state management

koalamango

Jessie W.

Posted on March 19, 2024

Fix Next.js 14 hydration error with Zustand state management

This post was originally published in my Medium blog.


With the release of Next.js 14, developers now have access to a range of powerful and experimental features. However, as we venture into these new functions, unique challenges arise such as hydration errors. This kind of error typically occurs when there’s a discrepancy in the rendered output due to how state is passed between the client and server.

🔖 TL;DR: You can find my example repo with the finished code here 😊


In this article, I’ll explain how persistent state works in Zustand with the Next.js App Router and implement a solution for resolving hydration errors.

Zustand: an alternative state management solution for React

Zustand simplifies state management for React applications. It’s light-weight and high performance, making it a popular choice among developers. Implementing is straightforward. Here’s a quick example to illustrate how it works. Firstly, install create next app with zustand:

npx create-next-app@latest
npm install zustand
Enter fullscreen mode Exit fullscreen mode

Zustand share state between components using the persist middleware. This feature is particularly useful when working with local storage on the client side. A typical use case looks like this:

// store.ts
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface BearState {
  bears: number;
  increaseBears: () => void;
  decreaseBears: () => void;
  removeAllBears: () => void;
}

export const useBearStore = create<BearState>()(
  persist(
    (set) => ({
      bears: 0,
      increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
      decreaseBears: () => set((state) => ({ bears: state.bears - 1 })),
      removeAllBears: () => set({ bears: 0 }),
    }),
    {
      name: "bear-store", // default to LocalStorage
    }
  )
);
Enter fullscreen mode Exit fullscreen mode

LocalStorage only available on the client side. When Next.js renders, it renders twice, first on then server and then client side. If we directly reference data in LocalStorage in our components, Next.js may detect that the content of our component when rendered is not the same during both renders, giving us a Hydration Error.

The Hydration Error in Next.js

This error typically arises when information used for component state is not passed between the client and the server, leading to a discrepancy in the rendered output. Next.js will tell you:

Nextjs runtime error

This example shows how we can use the bear-store from local storage above in our components to demonstrate this problem.

// hydration.tsx
"use client"; // (a)

import * as React from "react";
import { useBearStore } from "@/contexts/store";

const Hydration = () => {
  React.useEffect(() => {
    useBearStore.persist.rehydrate();
  }, []); // (b)

  return null;
};

export default Hydration;
Enter fullscreen mode Exit fullscreen mode
// layout.tsx
import type { Metadata } from "next";
import Hydration from "@/contexts/hydration"; // (c)

export const metadata: Metadata = {
  title: "Next Zustand Demo",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html>
      <body>
        <Hydration />
        <main>{children}</main>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

a) Create client side component called hydration.tsx
b) Call persist state inside useEffect hook — this is to make sure it runs on client side
c) Import it into root layout.

When the code is run on server side, useBearStore.bears will always be 0, as we are managing our state in LocalStorage. When you first load the page, this won’t be a problem, as it will be initialised the same way on the client side. However, it can be resolved quite simply as follows:

// useState, useEffect hook
const [hydrated, setHydrated] = useState<boolean>(false);
useEffect(() => {
  setHydrated(true);
}, []);

// Render when it's hydrated
{isHydrated ? bears : "Loading..."}
Enter fullscreen mode Exit fullscreen mode

On the initial hydration, we set a boolean variable named hydrated to false, indicating that hydration has not yet completed. On the initial render, both client and server side, the application will therefore render the text “Loading…” as the isHydrated check returns false. As the text is the same regardless of where it is rendered, we won’t see a Hydration Error.

Once hydration is completed, useEffect() sets the boolean to true, causing the component to re-render. This time as hydrated is now true, we can render the correct value of bears safely, showing the expected number to our user.

Demo

I have a working demo of this code available at https://next-zustand-starter.vercel.app/

When you land on the initial page, you will see the following and be able to click the buttons to control the bear-store kept in LocalStorage. As you click, the number of bears in the forest will change in realtime.

Bear component main page

When you click the “another page” link, you will be taken to another page with a new component. This renders initially without the correct number of bears, but once hydrated on the browser will re-render, having retrieved the correct value from LocalStorage. You should be able to switch between the two pages seamlessly, with the data remaining in sync.

Bear component on another page

That’s it

Happy Coding!

Resources

Github: https://github.com/koalamango/next-zustand
Online demo: https://next-zustand-starter.vercel.app/
Nextjs: https://nextjs.org/docs/messages/react-hydration-error
Zustand: https://github.com/pmndrs/zustand

💖 💪 🙅 🚩
koalamango
Jessie W.

Posted on March 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related