Zustand State
Lightweight state management for Next.js
🐻 What is Zustand?
Zustand is a small, fast, and scalable state management solution. It uses hooks for simple state management without providers or complex setup, making it perfect for Next.js applications that need lightweight global state.
// Simple Zustand store
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}))
// Use in component
const count = useStore((state) => state.count)
Key Zustand Features
Hook-Based
Use state with simple hooks
const bears = useStore(
state => state.bears
)
No Providers
No context providers needed
// Just import and use
import { useStore } from './store'
Fast & Small
Minimal bundle size
// Only 1KB gzipped
import { create } from 'zustand'
Middleware
Persist, devtools, and more
import { persist } from
'zustand/middleware'
🔹 Create a Basic Store
Create a Zustand store using the create function. Define your state and actions in one place without reducers or action types.
// lib/store.js
import { create } from 'zustand'
export const useCounterStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
// components/Counter.jsx
'use client'
import { useCounterStore } from '@/lib/store'
export default function Counter() {
const count = useCounterStore((state) => state.count)
const increment = useCounterStore((state) => state.increment)
const decrement = useCounterStore((state) => state.decrement)
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Output:
Count: 0
🔹 Async Actions
Handle async operations directly in your store actions. Zustand makes it simple to fetch data and update state without extra middleware or thunks.
// lib/userStore.js
import { create } from 'zustand'
export const useUserStore = create((set) => ({
users: [],
loading: false,
error: null,
fetchUsers: async () => {
set({ loading: true, error: null })
try {
const response = await fetch('/api/users')
const data = await response.json()
set({ users: data, loading: false })
} catch (error) {
set({ error: error.message, loading: false })
}
}
}))
// components/UserList.jsx
'use client'
import { useEffect } from 'react'
import { useUserStore } from '@/lib/userStore'
export default function UserList() {
const { users, loading, fetchUsers } = useUserStore()
useEffect(() => {
fetchUsers()
}, [fetchUsers])
if (loading) return <div>Loading...</div>
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
🔹 Persist State with Middleware
Use the persist middleware to save state to localStorage automatically. Your state survives page refreshes and browser sessions without extra code.
// lib/cartStore.js
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create(
persist(
(set) => ({
items: [],
addItem: (item) => set((state) => ({
items: [...state.items, item]
})),
removeItem: (id) => set((state) => ({
items: state.items.filter(item => item.id !== id)
})),
clearCart: () => set({ items: [] })
}),
{
name: 'cart-storage' // localStorage key
}
)
)
// components/Cart.jsx
'use client'
import { useCartStore } from '@/lib/cartStore'
export default function Cart() {
const items = useCartStore((state) => state.items)
const addItem = useCartStore((state) => state.addItem)
return (
<div>
<h2>Cart ({items.length})</h2>
<button onClick={() => addItem({ id: 1, name: 'Product' })}>
Add Item
</button>
</div>
)
}
🔹 Slices Pattern for Large Stores
Split large stores into smaller slices for better organization. Combine slices to create a single store while keeping code modular and maintainable.
// lib/slices/userSlice.js
export const createUserSlice = (set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null })
})
// lib/slices/cartSlice.js
export const createCartSlice = (set) => ({
items: [],
addToCart: (item) => set((state) => ({
items: [...state.items, item]
}))
})
// lib/store.js
import { create } from 'zustand'
import { createUserSlice } from './slices/userSlice'
import { createCartSlice } from './slices/cartSlice'
export const useStore = create((...a) => ({
...createUserSlice(...a),
...createCartSlice(...a)
}))
// Use in component
const user = useStore((state) => state.user)
const items = useStore((state) => state.items)
🔹 Selecting State Efficiently
Select only the state you need to prevent unnecessary re-renders. Zustand automatically optimizes updates when you select specific state slices.
// ❌ Bad - Re-renders on any state change
const store = useStore()
// ✅ Good - Only re-renders when count changes
const count = useStore((state) => state.count)
// ✅ Good - Select multiple values
const { count, increment } = useStore((state) => ({
count: state.count,
increment: state.increment
}))
// ✅ Best - Use shallow for objects
import { shallow } from 'zustand/shallow'
const { count, increment } = useStore(
(state) => ({ count: state.count, increment: state.increment }),
shallow
)