Managing global state in a React app can get messy fast. Context API is sometimes too basic, Redux can be overkill, and Recoil or Jotai might feel unfamiliar. If you’re looking for a minimal, scalable, and TypeScript-friendly state management tool for your Next.js app, Zustand is a dream come true.
In this post, Iβll walk you through how to set up and use Zustand in a Next.js project with TypeScriptβstep by step.
π€ What is Zustand?
Zustand (German for “state”) is a tiny, fast, and scalable state management library for React. Created by the developers of Jotai and React Spring, it offers a simple API with zero boilerplate and excellent TypeScript support.
Why choose Zustand?
- β Minimal setup
- β‘ Super fast
- π Works with server and client
- π TypeScript support out of the box
- π§ Built-in middleware like persist and devtools
βοΈ Step 1: Set Up Your Next.js + TypeScript App
If you havenβt already created a Next.js app:
npx create-next-app@latest zustand-demo --typescript
cd zustand-demo
Then install Zustand:
npm install zustand
π» Step 2: Create a Zustand Store
Letβs create a simple counter store.
Create a new file: stores/counterStore.ts
import { create } from 'zustand';
interface CounterState {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
Here:
- We define a
CounterState
interface for strong typing. - The store has three actions and one state variable (
count
).
π» Step 3: Use the Store in a Component
In pages/index.tsx
, use the state:
import { useCounterStore } from '@/stores/counterStore';
export default function Home() {
const { count, increase, decrease, reset } = useCounterStore();
return (
<main className="min-h-screen flex flex-col items-center justify-center text-center p-4">
<h1 className="text-4xl font-bold mb-4">Zustand + Next.js π₯</h1>
<p className="text-2xl mb-4">Count: {count}</p>
<div className="space-x-2">
<button onClick={increase} className="px-4 py-2 bg-green-500 text-white rounded">+</button>
<button onClick={decrease} className="px-4 py-2 bg-red-500 text-white rounded">-</button>
<button onClick={reset} className="px-4 py-2 bg-gray-500 text-white rounded">Reset</button>
</div>
</main>
);
}
Boom π₯ β live global state with zero prop drilling or context nesting.
π Bonus: Zustand Persist Middleware
Want to persist state across reloads? Use the persist
middleware.
npm install zustand/middleware
Then update the store like this:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface CounterState {
count: number;
increase: () => void;
decrease: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>()(
persist(
(set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}),
{
name: 'counter-storage', // localStorage key
}
)
);
Now your counter value will persist between refreshes!
π§ Bonus: Zustand + Next.js SSR
Zustand is client-side by default, but you can hydrate it for SSR if needed. However, for most use cases (UI state, filters, settings, etc.), client-only is perfectly fine.
β Final Thoughts
Zustand is a lightweight, powerful solution that plays beautifully with Next.js and TypeScript. It gives you just enough to manage state without all the Redux ceremony or Context API verbosity.
Perfect for:
- UI state
- Global modals
- Dark mode toggles
- Auth/session state
- Cart logic (e-commerce)
π§ͺ Want More?
Let me know if you want:
- Zustand with middleware logging
- Zustand with auth/session handling
- Zustand + server actions or Supabase sync
- Zustand across multiple stores
Happy coding! π§ β¨
Let the bear manage your state so you can focus on building cool stuff.