Zustand in Next.js 14 (with ts)

Saleh Akhgar
5 min readMay 5, 2024

--

Photo by AltumCode on Unsplash

In this post, I’ll explain how to use Zustand in a Next.js project. Zustand — what is it? According to their documentation:

“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.”

Atomic vs Boilerplate…

It really depends on the situation! In React.js or Next.js, you have the option to choose between Boilerplate or Atomic state management, depending on the project, team, complicity, etc.

My experience has led me to choose Atomic State Management. We were able to develop each page/directory independently without being dependent on other parts of the project. Here is an example of what I mean by “zero dependency on other parts”.

Let’s start with a new project

npx create-next-app@latest
What is your project named? 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? @/

Add Zustand

npm install zustand # or yarn add zustand or pnpm add zustand

Now we’re ready to develop our first page

Let’s add a new page to start

// Directory: /app/bears/page.tsx

export default function Page() {
return (
<main>
<h1>Bears</h1>
<p>How many Bears are in our Store? {0}</p>

<button>Increase</button>
<button>Decrease</button>
</main>
);
}

Our store will be created inside this page

In this example, we’ll create the first store within the page directory. It will only be used within this page and other pages and components won’t need it at all (that’s what I like about Atomic State Managements).

// Directory: /app/bears/_store/index.ts
import { create } from 'zustand';

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

// useBearStore
export const useBearStore = create<States>(() => ({
bears: 0,
}));

Call the state inside page

// Directory: /app/bears/page.tsx

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

export default function Page() {
// This will fetch everything inside the store.
// Kepp in mind that it will cause the component to update on every state change!
const store = useBearStore();

return (
<main>
<h1>Bears</h1>
<p>How many Bears are in our Store? {store.bears}</p>

<button>Increase</button>
<button>Decrease</button>
</main>
);
}

Or

// Directory: /app/bears/page.tsx

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

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

return (
<main>
<h1>Bears</h1>
<p>How many Bears are in our Store? {bears}</p>

<button>Increase</button>
<button>Decrease</button>
</main>
);
}

Adding our two actions to change the state

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

import { create } from 'zustand';

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

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

// useBearStore
export const useBearStore = create<States & Actions>((set) => ({
bears: 0,

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

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

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

return (
<main>
<h1>Bears</h1>
<p>How many Bears are in our Store? {bears}</p>

<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
</main>
);
}

Now that it’s ready, we can see how it works

npm run dev

How to use Persist Data

In the first step, we will change our store to handle the persist data. You can choose from localStorage, AsyncStorage, IndexedDB, etc.

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

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

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

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

// useBearStore
export const useBearStore = create(
persist<States & Actions>(
(set) => ({
bears: 0,

increase: () => set((state) => ({ bears: state.bears + 1 })),
decrease: () => set((state) => ({ bears: state.bears - 1 })),
}),
{
name: 'bearStore',
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

// /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/bears/page.tsx

'use client';
import usePersistStore from '@/helpers/usePersistStore';
import { useBearStore } from './_store';

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

return (
<main>
<h1>Bears</h1>
<p>How many Bears are in our Store? {store?.bears}</p>

<button onClick={store?.increase}>Increase</button>
<button onClick={store?.decrease}>Decrease</button>
</main>
);
}

Check out the code on GitHub and give it a try…

https://github.com/SalehAkaJim/medium-zustand

--

--