Zustand in Next.js 14

Ijlal Windhi
5 min readJun 27, 2024

--

Hi all!

Thank you for taking the time to read this article. On this occasion I will explain how to implement Zustand in Next.js 14 using Typescript. Maybe some of you don’t know what Zustand is. From the official documentation they mention:

“A small, fast, and scalable bearbones state management solution. Zustand has a comfy API based on hooks. It isn’t boilerplatey or opinionated, but has enough convention to be explicit and flux-like.”

Which is better, atomic or boilerplate?

Both methods are valid for state management and the best choice depends on the specific context, such as your team’s preferences, project requirements, and complexity. React.js and Next.js offer both atomic and boilerplate solutions.

Personally, I lean towards Atomic State Management because it allows me to create self-contained state logic for each page or directory, without impacting other parts of the application.

Let me illustrate what I mean by “without impacting other parts” using a Zustand example below.

If you still don’t understand Atomic, you can read my previous article

Project Initialization

Open your terminal and navigate to the directory you want, follow the steps below:

npx create-next-app@latest
What is your project named? example-zustand
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use src/ directory? No
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias (@/)? Yes
What import alias would you like configured? @/

Install Zustand

# NPM
npm install zustand
# YARN
yarn add zustand
# PNPM
pnpm add zustand

After you have succeeded in adding Zustand dependencies to your project, now we are ready to start developing our project using Zustand.

Let’s create a new page

// Directory: /app/counter/page.tsx

export default function Page() {
return (
<main className="flex flex-col gap-4 items-center justify-center min-h-screen">
<h1>
Counter <span>{0}</span>
</h1>

<div className="flex gap-2">
<button className="border border-white p-1.5 font-medium rounded-md">
Increase
</button>
<button className="border border-white p-1.5 font-medium rounded-md">
Decrease
</button>
</div>
</main>
);
}
initial page counter

Create a store on the page

In this case, we’ll make a dedicated data store for this page only. Other parts of the website won’t be able to access it, which is a key benefit of using Atomic State Management.

// Directory: /app/counter/_store/index.ts

import { create } from 'zustand';

// State types
interface States {
count: number;
}

// useCounterStore
export const useCountStore = create<States>(() => ({
count: 0,
}));

Call the state on the page

Create an action to change the state

// Directory: /app/counter/page.tsx

'use client';
import { useCountStore } from './_store';

export default function Page() {
// Or, we can fetch what we need from the store
const { count } = useCountStore((state) => state);

return (
<main className="flex flex-col gap-4 items-center justify-center min-h-screen">
<h1>
Counter <span>{count}</span>
</h1>

<div className="flex gap-2">
<button className="border border-white p-1.5 font-medium rounded-md">
Increase
</button>
<button className="border border-white p-1.5 font-medium rounded-md">
Decrease
</button>
</div>
</main>
);
};

We have succeeded in creating a state using Zustand. Next we will learn how to create actions or how to change values ​​in a state. Follow these steps

// Directory: /app/counter/_store/index.ts

import { create } from 'zustand';

// State types
interface States {
count: number;
}

// Action types
interface Actions {
increase: () => void;
decrease: () => void;
}

// useCounterStore
export const useCountStore = create<States & Actions>((set) => ({
// States
count: 0,

// Actions
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}));
// Directory: /app/counter/page.tsx

'use client';
import { useCountStore } from './_store';

export default function Page() {

// Or, we can fetch what we need from the store
const { count, decrease, increase } = useCountStore((state) => state);

return (
<main className="flex flex-col gap-4 items-center justify-center min-h-screen">
<h1>
Counter <span>{count}</span>
</h1>
<div className="flex gap-2">
<button onClick={increase} className="border border-white p-1.5 font-medium rounded-md">
Increase
</button>
<button onClick={decrease} className="border border-white p-1.5 font-medium rounded-md">
Decrease
</button>
</div>
</main>
);
}

We have done everything to create state management using Zustand, now we can see the results of the code we have created by running the following command

npm run dev

After the server is running you can access it at the URL http://localhost:3000

How to use Persist Data

To start, we’ll modify our store to handle persistent data. You can choose a storage mechanism like localStorage, AsyncStorage, or IndexedDB.

// Directory: /app/counter/_store/index.ts

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

// State types
interface States {
count: number;
}

// Action types
interface Actions {
increase: () => void;
decrease: () => void;
}

// useCounterStore
export const useCountStore = create(
persist<States & Actions>(
(set) => ({
// States
count: 0,
// Actions
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}),
{
name: 'count-store',
storage: createJSONStorage(() => localStorage),
}
)
);

The best way to get persist data into condition (avoiding hydration errors)

For fetching persist data, we’ll create a custom hook

// Directory: /helpers/usePersistStore/index.ts

import { useState, useEffect } from 'react';

const usePersistStore = <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;
};

export default usePersistStore;

And our page will be:

// Directory: /app/counter/page.tsx

'use client';
import { useCountStore } from './_store';
import usePersistStore from "@/helpers/usePersistStore";
export default function Page() {
// Or, we can fetch what we need from the store

// remark below if you don't want to use persist store
// const { count, decrease, increase } = useCountStore((state) => state);

const store = usePersistStore(useCountStore, (state) => state);

return (
<main className="flex flex-col gap-4 items-center justify-center min-h-screen">
<h1>
Counter <span>{store?.count}</span>
</h1>
<div className="flex gap-2">
<button onClick={store?.increase} className="border border-white p-1.5 font-medium rounded-md">
Increase
</button>
<button onClick={store?.decrease} className="border border-white p-1.5 font-medium rounded-md">
Decrease
</button>
</div>
</main>
);
}

Congratulations, you have successfully created a project using Zustand as state management. I hope that this article can be useful for you and your team and make your knowledge even wider!

Thank you and see you in other articles.

If you want to see the complete code, you can visit the following repository https://github.com/ijlalWindhi/medium-zustand

--

--

Ijlal Windhi
Ijlal Windhi

Written by Ijlal Windhi

👨‍💻Technology enthusiast | Fullstack developer | Software engineering 📱For business : ijlalwindhisa@gmail.com

Responses (2)