Simplifying State Management in React with Zustand

Simplifying State Management in React with Zustand



Managing state in React applications has always been a challenge. For small apps, useState and useReducer work fine, but as your app grows, so does the complexity of managing state across multiple components. Traditionally, libraries like Redux or the Context API have been used to handle global state. However, they often come with boilerplate code, performance issues, or steep learning curves.

This is where Zustand comes in.

Zustand is a lightweight, scalable, and minimal state management library for React that lets you manage state with ease—without unnecessary boilerplate. In this blog, we’ll explore why Zustand is a great choice, how to use it, compare it with Redux, and build a real-world example.

🚀 What is Zustand?

Zustand (German for “state”) is a small state management library created by the same team behind Jotai and Valtio.

Key highlights:

  • Tiny size – less than 1kb gzipped.
  • Minimal boilerplate – state is just a hook.
  • Performance focused – avoids unnecessary re-renders.
  • Scalable – works for both small apps and large-scale projects.

Unlike Redux, which forces a lot of patterns and structure, Zustand gives you freedom. You define your state in a store and use it directly in your components—simple and elegant.

🛠 Getting Started with Zustand

Installation

npm install zustand

Creating a Store

With Zustand, you create a store using the create function.

import { create } from 'zustand'

const useStore = create((set) => ({ 
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })), 
  decrease: () => set((state) => ({ count: state.count - 1 })),
}))

Here:

  • count is the state.
  • increase and decrease are actions that update the state.
  • set is Zustand’s way of updating state (similar to React’s setState).

🎨 Using Zustand in Components

function Counter() {
  const { count, increase, decrease } = useStore() 
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increase}>+</button>
      <button onClick={decrease}>-</button>
    </div>
  )
}

That’s it! No reducers, no dispatch, no boilerplate.

📦 Advanced Features of Zustand

Persisting State

import { create } from 'zustand'
import { persist } from 'zustand/middleware'

const useAuthStore = create(
  persist(
    (set) => ({ 
      user: null,
      login: (user) => set({ user }),
      logout: () => set({ user: null }),
    }),
    { name: 'auth-storage' }
  )
)

Now your state survives page reloads.

Async Actions (Fetching Data)

const useTodoStore = create((set) => ({ 
  todos: [],
  fetchTodos: async () => { 
    const response = await fetch('https://jsonplaceholder.typicode.com/todos') 
    const data = await response.json()
    set({ todos: data })
  },
}))

Slices for Large Applications

const createUserSlice = (set) => ({ 
  user: null,
  setUser: (user) => set({ user }),
})

const createThemeSlice = (set) => ({ 
  theme: 'light',
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
})

const useBoundStore = create((...a) => ({
  ...createUserSlice(...a),
  ...createThemeSlice(...a),
}))

⚖️ Redux vs Zustand – A Quick Comparison

Let’s take a simple counter app and see how it looks in both libraries.

🟥 Redux

// store.js


import { createStore } from 'redux' 

const initialState = { count: 0 }

function counterReducer(state = initialState, action) { 
switch (action.type) {
  case 'INCREMENT': return { count: state.count + 1 } 
  case 'DECREMENT': return { count: state.count - 1 } 
  default: return state
 }
}

export const store = createStore(counterReducer)

// Counter.js


import { useSelector, useDispatch } from 'react-redux'

function Counter() {
  const count = useSelector((state) => state.count)
  const dispatch = useDispatch()

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
    </div>
  )
}

✔ Works great, but comes with reducers, actions, and boilerplate.

🟩 Zustand

// store.js


import { create } from 'zustand'

export const useStore = create((set) => ({ count: 0,
 increase: () => set((state) => ({ count: state.count + 1 })), 
 decrease: () => set((state) => ({ count: state.count - 1 })),
}))

// Counter.js


import { useStore } from './store'

function Counter() {
  const { count, increase, decrease } = useStore()

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increase}>+</button>
      <button onClick={decrease}>-</button>
    </div>
  )
}

✔ Much cleaner: no reducers, no actions, no extra boilerplate.

🏆 Verdict

Redux: Good for very large apps with strict patterns.
Zustand: Lightweight, simple, and perfect for most React projects.

🛒 Real-World Example: Shopping Cart with Zustand

Cart Store

import { create } from 'zustand'

export const useCartStore = create((set) => ({ 
  cart: [],
  addItem: (item) => set((state) => ({ cart: [...state.cart, item] })),
  removeItem: (id) => set((state) => ({ cart: state.cart.filter((item) => item.id !== id) })),
  clearCart: () => set({ cart: [] }),
}))

Components

ProductList.js


import { useCartStore } from './cartStore'
const products = [
  { id: 1, name: 'Laptop', price: 1200 },
  { id: 2, name: 'Headphones', price: 100 },
  { id: 3, name: 'Phone', price: 800 },
]

function ProductList() {
  const addItem = useCartStore((state) => state.addItem)
  return (
    <div>
      <h2>Products</h2>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            {product.name} - ${product.price}
            <button onClick={() => addItem(product)}>Add</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

Cart.js


import { useCartStore } from './cartStore'
function Cart() {
  const { cart, removeItem, clearCart } = useCartStore()
  return (
    <div>
      <h2>Shopping Cart</h2>
      {cart.length === 0 && <p>Your cart is empty.</p>}
      <ul>
        {cart.map((item) => (
          <li key={item.id}>
            {item.name} - ${item.price}
            <button onClick={() => removeItem(item.id)}>-</button>
          </li>
        ))}
      </ul>
      {cart.length > 0 && <button onClick={clearCart}>Clear Cart</button>}
    </div>
  )
}

Conclusion

Zustand is one of the simplest yet most powerful state management libraries for React.

  • It removes Redux’s boilerplate.
  • It performs better than Context API
  • It scales from small apps to enterprise-level projects.
  • If you’re tired of complicated state management, give Zustand a try—you might never look back.