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
)

🧠 Test Your Knowledge

Does Zustand require a Provider component?