Next.js with Zustand — State Management
Advertisement
Next.js with Zustand — State Management
Zustand provides a minimal, flexible approach to state management that works perfectly with Next.js and React.
- Next.js with Zustand — State Management
- Installation
- Basic Store
- Using in Components
- User Store Example
- Persist State
- Multiple Stores
- Complex State
- DevTools Integration
- Immer Middleware
- Real-World: Shopping Cart
- Async Actions
- FAQ
Installation
npm install zustand
Basic Store
Create a store:
// lib/store.ts
import { create } from 'zustand'
interface Store {
count: number
increment: () => void
decrement: () => void
}
export const useCountStore = create<Store>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 }))
}))
Using in Components
// app/components/counter.tsx
'use client'
import { useCountStore } from '@/lib/store'
export function Counter() {
const { count, increment, decrement } = useCountStore()
return (
<div className="flex items-center gap-4">
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}
User Store Example
// lib/stores/user.ts
import { create } from 'zustand'
interface User {
id: string
email: string
name: string
role: 'user' | 'admin'
}
interface UserStore {
user: User | null
setUser: (user: User) => void
clearUser: () => void
isAdmin: () => boolean
}
export const useUserStore = create<UserStore>((set, get) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
isAdmin: () => {
const { user } = get()
return user?.role === 'admin'
}
}))
Persist State
Persist state to localStorage:
// lib/stores/theme.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface ThemeStore {
theme: 'light' | 'dark'
toggleTheme: () => void
}
export const useThemeStore = create<ThemeStore>(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}))
}),
{
name: 'theme-storage'
}
)
)
Multiple Stores
Combine stores:
// lib/useAppStore.ts
import { useUserStore } from './stores/user'
import { useThemeStore } from './stores/theme'
import { useNotificationStore } from './stores/notifications'
export function useAppStore() {
return {
user: useUserStore(),
theme: useThemeStore(),
notifications: useNotificationStore()
}
}
Usage:
const { user, theme } = useAppStore()
Complex State
Handle complex state updates:
// lib/stores/todo.ts
import { create } from 'zustand'
interface Todo {
id: string
text: string
completed: boolean
}
interface TodoStore {
todos: Todo[]
addTodo: (text: string) => void
removeTodo: (id: string) => void
toggleTodo: (id: string) => void
updateTodo: (id: string, text: string) => void
}
export const useTodoStore = create<TodoStore>((set) => ({
todos: [],
addTodo: (text) =>
set((state) => ({
todos: [...state.todos, { id: Date.now().toString(), text, completed: false }]
})),
removeTodo: (id) =>
set((state) => ({
todos: state.todos.filter((todo) => todo.id !== id)
})),
toggleTodo: (id) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
})),
updateTodo: (id, text) =>
set((state) => ({
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, text } : todo
)
}))
}))
DevTools Integration
Enable debugging:
// lib/stores/debug.ts
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
interface DebugStore {
count: number
increment: () => void
}
export const useDebugStore = create<DebugStore>(
devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}), { name: 'DebugStore' })
)
Use Redux DevTools browser extension to debug.
Immer Middleware
Simplify state updates:
// lib/stores/cart.ts
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface CartItem {
id: string
quantity: number
price: number
}
interface CartStore {
items: CartItem[]
addItem: (item: CartItem) => void
removeItem: (id: string) => void
updateQuantity: (id: string, quantity: number) => void
}
export const useCartStore = create<CartStore>(
immer((set) => ({
items: [],
addItem: (item) =>
set((state) => {
state.items.push(item)
}),
removeItem: (id) =>
set((state) => {
state.items = state.items.filter((item) => item.id !== id)
}),
updateQuantity: (id, quantity) =>
set((state) => {
const item = state.items.find((item) => item.id === id)
if (item) item.quantity = quantity
})
}))
)
Real-World: Shopping Cart
// lib/stores/shopping.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface CartItem {
productId: string
quantity: number
price: number
}
interface ShoppingStore {
cart: CartItem[]
addToCart: (productId: string, price: number) => void
removeFromCart: (productId: string) => void
clearCart: () => void
getTotal: () => number
}
export const useShoppingStore = create<ShoppingStore>(
persist(
(set, get) => ({
cart: [],
addToCart: (productId, price) =>
set((state) => {
const existing = state.cart.find((item) => item.productId === productId)
if (existing) {
return {
cart: state.cart.map((item) =>
item.productId === productId
? { ...item, quantity: item.quantity + 1 }
: item
)
}
}
return {
cart: [...state.cart, { productId, quantity: 1, price }]
}
}),
removeFromCart: (productId) =>
set((state) => ({
cart: state.cart.filter((item) => item.productId !== productId)
})),
clearCart: () => set({ cart: [] }),
getTotal: () => {
const { cart } = get()
return cart.reduce((total, item) => total + item.price * item.quantity, 0)
}
}),
{ name: 'shopping-store' }
)
)
Async Actions
// lib/stores/posts.ts
import { create } from 'zustand'
interface Post {
id: string
title: string
}
interface PostsStore {
posts: Post[]
loading: boolean
error: string | null
fetchPosts: () => Promise<void>
}
export const usePostsStore = create<PostsStore>((set) => ({
posts: [],
loading: false,
error: null,
fetchPosts: async () => {
set({ loading: true })
try {
const response = await fetch('/api/posts')
const data = await response.json()
set({ posts: data, error: null })
} catch (error) {
set({ error: (error as Error).message })
} finally {
set({ loading: false })
}
}
}))
FAQ
Q: Should I use Zustand or Context API? A: Zustand for global, frequently-updated state. Context for rare updates or small scopes.
Q: Can I use Zustand without 'use client'? A: No, stores must be used in Client Components where hooks work.
Q: How do I handle async state? A: Create async actions that call set when complete. Use immer for easier mutations.
Zustand provides powerful, minimal state management for Next.js applications.
Advertisement