Skip to content
Feb 28

Zustand State Management

MT
Mindli Team

AI-Generated Content

Zustand State Management

Managing state in React applications has evolved from prop drilling through Context to robust external libraries. While solutions like Redux offer powerful predictability, they often introduce significant boilerplate and conceptual overhead for simpler applications. Zustand (German for "state") is a minimalist, hook-based state management library that strips away complexity while maintaining flexibility. It provides a straightforward API to create and consume stores, making it an excellent choice for developers seeking a lightweight yet capable solution that scales from small to medium-sized projects.

Core Concept: The Zustand Store

At the heart of Zustand is the store. You create one using the create function, which defines the initial state and the functions (actions) to update it. Unlike Redux, there is no dispatcher, reducers, or action creators. A store in Zustand is a self-contained unit where state is updated directly via setter functions.

Here is the basic syntax for creating a store:

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

The create function receives a set function as its argument. This set function is used within your actions to update the store's state. It can accept a new state object directly or an updater function that receives the previous state, as shown in increasePopulation. This model is incredibly intuitive because it feels like managing local component state, but globally.

Accessing and Using State with Hooks

The primary way you interact with a Zustand store is through a custom hook. The store you create (like useBearStore) is a React hook. You can call it anywhere in your component tree, and it will subscribe your component to the pieces of state it selects. This eliminates the need for providers wrapping your application, though you can still use them if desired.

To access the entire state, you simply call the hook:

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

More powerfully, you can select specific slices of state. This is a critical performance feature because your component will only re-render when the selected slice changes. For example, a component that only uses the increasePopulation action won't re-render when the bears count changes.

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>Add bear</button>
}

This selective subscription model is efficient and declarative, mirroring the best practices of other state management libraries but with far less code.

Advanced Features: Middleware and Integrations

While Zustand's core is simple, it is designed to be extended through a middleware pattern. Middleware allows you to wrap the set function to intercept state updates and add custom logic. Common use cases include logging, persisting state to local storage, or implementing immutable updates with Immer.

For instance, adding persistence (saving state to localStorage) is often a one-line addition using the persist middleware:

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

const useStore = create(
  persist(
    (set, get) => ({
      items: [],
      addItem: (item) => set((state) => ({ items: [...state.items, item] })),
    }),
    {
      name: 'food-storage', // unique name for the localStorage key
    }
  )
)

The persist middleware automatically syncs your store's state to the specified storage engine. Another vital middleware is devtools, which integrates with the Redux DevTools browser extension, allowing you to inspect state changes, travel through time, and debug your application visually. These integrations showcase how Zustand delivers advanced capabilities without compromising its core simplicity.

State Updates and Immutability

A key principle in React state management is immutability—you should not mutate state objects directly. Zustand's set function enforces this by merging the updated fields into a new state object. However, for nested state updates, manual spreading can become verbose. This is where the Immer middleware becomes exceptionally useful.

By wrapping your store with the immer middleware, you can write state updates that appear to mutate the state directly, but which produce a correct immutable update under the hood.

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

const useStore = create(
  immer((set) => ({
    user: {
      name: 'Alice',
      profile: { level: 1 },
    },
    levelUp: () =>
      set((state) => {
        state.user.profile.level += 1 // This is now safe!
      }),
  }))
)

This approach significantly reduces the cognitive load and code clutter associated with deeply nested state updates, making your actions cleaner and more readable.

Common Pitfalls

  1. Over-selecting State in Components: A common mistake is selecting the entire store object when a component only needs a small part of it, like const store = useBearStore(). This causes the component to re-render on every change to any part of the store state, hurting performance. Always use the selector function to pick only the specific state slices or actions your component needs.
  1. Creating Unnecessary Stores: Zustand's simplicity might tempt you to create a new store for every small piece of state. This can fragment your state logic and make data flow harder to trace. Instead, group related state and actions into logical domain stores (e.g., useAuthStore, useCartStore). If state is truly local to a component or a small subtree, prefer useState or useReducer.
  1. Direct Mutation Without Immer: Without the Immer middleware, you must be diligent about returning new objects from your set calls. Accidentally mutating the existing state directly will not trigger re-renders and can lead to subtle bugs.

// ❌ Wrong: Direct mutation set((state) => { state.bears += 1 return state // This mutation won't be detected! })

// ✅ Correct: Return a new object set((state) => ({ bears: state.bears + 1 }))

// ✅ Also Correct: Using Immer middleware set((state) => { state.bears += 1 })

  1. Forgetting Middleware Order: When applying multiple middleware, their order matters. The general rule is that the first middleware in the chain is applied first to your set function. For a typical setup, you might want: devtools > persist > immer > your store logic. Always check the library documentation for the correct composition.

Summary

  • Zustand provides a minimal, boilerplate-free API for global state management in React, using a familiar hook-based pattern.
  • You create a self-contained store with create, defining state and update functions that use a built-in set method.
  • Components access state by calling the store as a hook and using a selector function to subscribe to specific slices, optimizing performance by preventing unnecessary re-renders.
  • Its power is extended through middleware for logging, state persistence (e.g., to localStorage), integration with Redux DevTools, and simplified immutable updates using Immer.
  • While simple to adopt, effective use requires mindful state selection, logical store organization, and careful handling of immutable updates to avoid common performance and correctness issues.

Write better notes with AI

Mindli helps you capture, organize, and master any subject with AI-powered summaries and flashcards.