2 min 26 sec read

Master Zustand in Next.js: The Simple State Management Guide

Stop wrestling with Redux. Learn how to use Zustand for state management in Next.js and React. A complete guide to setup, persistence, and App Router integration.

logo
Maurya Patel

State Management Doesn't Have to Be Painful

If you have ever felt overwhelmed by the setup for libraries like Redux or Recoil, you are not alone. Boilerplate code, complex reducers, and provider wrapping can make simple apps feel heavy.

Enter Zustand (German for 'state').

Zustand is a small, fast, and scalable "bearbones" state-management solution. It is the perfect boilerplate-free alternative for modern React and Next.js applications.

Why Developers Are Switching to Zustand

  • Zustand creates a global store using a straightforward API based on hooks. Here is why it is winning over the React ecosystem:
  • No Context Provider Hell: You don't need to wrap your app in <Provider> tags.
  • Zero Boilerplate: No action types or complex reducers. Just a store and functions.
  • Tiny Footprint: It is incredibly small (~1KB gzipped), keeping your bundle size fast.
  • Middleware Ready: Built-in support for persisting data to localStorage and Redux DevTools.

Step 1 : Install Zustand

Add the package to your Next.js or React project:

npm ‍​install ‍​zustand
# or
yarn ‍​add ‍​zustand

Step 2 : Create Your Store

Create a dedicated file for your store. A common convention is store/useCounterStore.ts.

Unlike Redux, you define your state and actions together in one hook:

useCounterStore.ts
1import ‍​{ ‍​create ‍​} ‍​from ‍​'zustand';
2
3// 1. Define the state shape and actions
4interface ‍​CounterState ‍​{
5 ‍​ ‍​count: ‍​number;
6 ‍​ ‍​increment: ‍​() ‍​=> ‍​void;
7 ‍​ ‍​decrement: ‍​() ‍​=> ‍​void;
8}
9
10// 2. Create the store
11export ‍​const ‍​useCounterStore ‍​= ‍​create<CounterState>((set) ‍​=> ‍​({
12 ‍​ ‍​count: ‍​0,
13 ‍​ ‍​increment: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​+ ‍​1 ‍​})),
14 ‍​ ‍​decrement: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​- ‍​1 ‍​})),
15}));
16
Tip: Zustand works great with both TypeScript (.ts) and plain JavaScript (.js).

Step 3 : Use It in Your Components

This is where Zustand shines. You can import the hook anywhere in your app—no providers needed.

Counter.tsx
1'use client'; ‍​// Required for App Router!
2
3import ‍​{ ‍​useCounterStore ‍​} ‍​from ‍​'../store/useCounterStore';
4
5export ‍​default ‍​function ‍​HomePage() ‍​{
6 ‍​ ‍​// 1. Get state and actions from the store
7 ‍​ ‍​const ‍​{ ‍​count, ‍​increment, ‍​decrement ‍​} ‍​= ‍​useCounterStore();
8
9 ‍​ ‍​return ‍​(
10 ‍​ ‍​ ‍​ ‍​<div ‍​style={{ ‍​padding: ‍​'20px', ‍​textAlign: ‍​'center' ‍​}}>
11 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<h1>Zustand ‍​Counter</h1>
12 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<p ‍​style={{ ‍​fontSize: ‍​'2rem', ‍​margin: ‍​'20px 0' ‍​}}>{count}</p>
13 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<div ‍​style={{ ‍​display: ‍​'flex', ‍​gap: ‍​'10px', ‍​justifyContent: ‍​'center' ‍​}}>
14 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<button ‍​onClick={increment}>Increment</button>
15 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​<button ‍​onClick={decrement}>Decrement</button>
16 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​</div>
17 ‍​ ‍​ ‍​ ‍​</div>
18 ‍​ ‍​);
19}
20

That’s it — your state is now global, reactive, and super easy to use.

Step 4 : Add Persistence (Save to LocalStorage)

Want your state to survive a page refresh? Zustand's persist middleware makes this trivial. It hooks right into localStorage.

You'll need to import persist from zustand/middleware.

useThemeStore.ts
1import ‍​{ ‍​create ‍​} ‍​from ‍​'zustand';
2import ‍​{ ‍​persist ‍​} ‍​from ‍​'zustand/middleware';
3
4interface ‍​CounterState ‍​{
5 ‍​ ‍​count: ‍​number;
6 ‍​ ‍​increment: ‍​() ‍​=> ‍​void;
7 ‍​ ‍​decrement: ‍​() ‍​=> ‍​void;
8}
9
10// Wrap your create function with the persist middleware
11export ‍​const ‍​useCounterStore ‍​= ‍​create(
12 ‍​ ‍​persist<CounterState>(
13 ‍​ ‍​ ‍​ ‍​(set) ‍​=> ‍​({
14 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​count: ‍​0,
15 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​increment: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​+ ‍​1 ‍​})),
16 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​decrement: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​- ‍​1 ‍​})),
17 ‍​ ‍​ ‍​ ‍​}),
18 ‍​ ‍​ ‍​ ‍​{
19 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​name: ‍​'counter-storage', ‍​// Name for the localStorage key
20 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​storage: ‍​createJSONStorage(() ‍​=> ‍​localStorage)
21 ‍​ ‍​ ‍​ ‍​}
22 ‍​ ‍​)
23);
24


Step 5: Debugging with Redux DevTools (Optional)

Even though you aren't using Redux, you can still use the Redux DevTools extension to debug your Zustand state.

import ‍​{ ‍​create ‍​} ‍​from ‍​'zustand';
import ‍​{ ‍​persist ‍​} ‍​from ‍​'zustand/middleware';
import ‍​{ ‍​devtools ‍​} ‍​from ‍​'zustand/middleware'; ‍​// Import devtools

interface ‍​CounterState ‍​{
 ‍​ ‍​count: ‍​number;
 ‍​ ‍​increment: ‍​() ‍​=> ‍​void;
 ‍​ ‍​decrement: ‍​() ‍​=> ‍​void;
}

// Stack middleware by wrapping one inside the other
export ‍​const ‍​useCounterStore ‍​= ‍​create(
 ‍​ ‍​devtools( ‍​// DevTools must be the outer wrapper
 ‍​ ‍​ ‍​ ‍​persist<CounterState>(
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​(set) ‍​=> ‍​({
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​count: ‍​0,
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​increment: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​+ ‍​1 ‍​})),
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​decrement: ‍​() ‍​=> ‍​set((state) ‍​=> ‍​({ ‍​count: ‍​state.count ‍​- ‍​1 ‍​})),
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​}),
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​{
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​name: ‍​'counter-storage', ‍​// Name for localStorage
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​ ‍​storage: ‍​createJSONStorage(() ‍​=> ‍​localStorage)
 ‍​ ‍​ ‍​ ‍​ ‍​ ‍​}
 ‍​ ‍​ ‍​ ‍​)
 ‍​ ‍​)
);

Now you can inspect and debug your Zustand state with Redux DevTools. 🧩

Bonus : Zustand in Next.js App Router

If you are using Next.js 13, 14, or 15 with the App Router, there is one rule you must follow:

Zustand hooks rely on React Context/Hooks, so they must run on the Client.

Any component importing your store must have the 'use client' directive at the very top of the file. If you try to use the store in a Server Component, your build will fail.

Best Practice: Keep your Server Components (pages/layouts) clean. Pass data down to Client Components, and let those Client Components interact with the Zustand store.

'use client'; ‍​// This line is essential!

import ‍​{ ‍​useCounterStore ‍​} ‍​from ‍​'../store/useCounterStore';

export ‍​default ‍​function ‍​HomePage() ‍​{
 ‍​ ‍​const ‍​{ ‍​count ‍​} ‍​= ‍​useCounterStore();
 ‍​ ‍​// ...
}

You can still share the store between client components — just make sure the store itself is defined outside the React component tree (e.g., in /store).

Conclusion

Zustand hits the sweet spot for state management. It offers the power of global state without the complexity of Redux.

If you want to keep your Next.js codebase clean, readable, and performant, Zustand is the modern choice.

Enjoyed this article?

Share it with your network or friends.