Fix Next.js 14 hydration error with Zustand state management
Tags: nextjs, zustand, react, typescript
Published: 2024-03-19 09:04:49

_This post was originally published in my [Medium](https://medium.com/@koalamango/fix-next-js-hydration-error-with-zustand-state-management-0ce51a0176ad) blog._

<hr />

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](https://github.com/koalamango/next-zustand) with the finished code here 😊

<hr />

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:

```js
npx create-next-app@latest
npm install zustand
```

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:

```js
// 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
    }
  )
);
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zvrodlu8pnnqjysyf36f.png)

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

```js
// 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;
```

```js
// 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>
  );
}
```

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:

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

// Render when it's hydrated
{isHydrated ? bears : "Loading..."}
```

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/](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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j287namg3hh7fwjtjd13.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zsg9bqb9cvyzec42xt1.png)

### 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