โ† Back to DevBytes

State Management in Zustand: Patterns and Libraries

Introduction to Zustand

State management is one of the most critical aspects of building robust React applications. While React's built-in useState and useContext hooks are sufficient for simple applications, they often lead to performance bottlenecks and complex component trees as your app scales. Enter Zustand, a small, fast, and scalable bearbones state-management solution.

Zustand (which means "state" in German) was created to simplify global state management without the boilerplate of Redux. It uses a hook-based API, eliminates the need for context providers, and allows you to subscribe to specific slices of state to prevent unnecessary re-renders.

Getting Started with Zustand

To begin using Zustand, you first need to install it in your React project. You can do this via npm or yarn:

npm install zustand
# or
yarn add zustand

Basic Store Creation

Creating a store in Zustand is incredibly straightforward. You define your state and the actions that mutate it inside a single function passed to create.

import { create } from 'zustand';

const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

Consuming State in Components

Because Zustand provides a custom hook, you can consume your state directly in any component without wrapping your application in a provider.

import React from 'react';
import useCounterStore from './useCounterStore';

function Counter() {
  // Destructuring the state and actions directly
  const { count, increment, decrement, reset } = useCounterStore();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

export default Counter;

Advanced Patterns in Zustand

Selectors and Performance Optimization

In the example above, the Counter component will re-render whenever any part of the store changes. To optimize performance, you should use selectors. Selectors allow a component to subscribe only to the specific slices of state it needs.

import React from 'react';
import useCounterStore from './useCounterStore';

function OptimizedCounter() {
  // The component will only re-render if 'count' changes
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

If you need to select multiple state slices, you can pass a shallow equality function to prevent re-renders when the object reference changes but the values remain the same.

import { create } from 'zustand';
import { shallow } from 'zustand/shallow';

// Inside a component:
const { count, increment } = useCounterStore(
  (state) => ({ count: state.count, increment: state.increment }),
  shallow
);

Async Actions

Unlike Redux, which requires middleware like Redux Thunk or Saga to handle asynchronous operations, Zustand handles async actions natively. You simply call set whenever your promise resolves.

import { create } from 'zustand';

const useUserStore = create((set) => ({
  users: [],
  isLoading: false,
  error: null,
  fetchUsers: async () => {
    set({ isLoading: true, error: null });
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await response.json();
      set({ users: data, isLoading: false });
    } catch (err) {
      set({ error: err.message, isLoading: false });
    }
  },
}));

Zustand Ecosystem and Libraries

Zustand comes with a rich set of built-in middleware and integrates well with other popular libraries in the React ecosystem.

Using Immer for Immutable Updates

Updating nested state objects immutably can be verbose. By wrapping your store with the immer middleware, you can mutate state directly in a "draft" state, and Immer will handle the immutable update for you.

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

const useTodoStore = create(immer((set) => ({
  todos: [],
  addTodo: (text) => set((state) => {
    // Mutating state directly thanks to Immer
    state.todos.push({ id: Date.now(), text, completed: false });
  }),
  toggleTodo: (id) => set((state) => {
    const todo = state.todos.find((t) => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
    }
  }),
})));

Persisting State

If you need to save your state to localStorage or sessionStorage, Zustand provides a persist middleware out of the box.

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

const useSettingsStore = create(
  persist(
    (set) => ({
      theme: 'light',
      toggleTheme: () => set((state) => ({ 
        theme: state.theme === 'light' ? 'dark' : 'light' 
      })),
    }),
    {
      name: 'theme-storage', // unique name for localStorage key
    }
  )
);

Redux DevTools Integration

For debugging, you can connect your Zustand store to the Redux DevTools extension using the devtools middleware.

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools(
    (set) => ({
      // your state and actions here
    }),
    { name: 'MyZustandStore' } // Name shown in DevTools
  )
);

Best Practices

Conclusion

Zustand has emerged as a powerful, ergonomic, and lightweight solution for state management in React. By stripping away the boilerplate of traditional state managers and embracing a hook-first API, it allows developers to build scalable applications with minimal friction. Whether you are managing simple UI toggles or complex asynchronous data flows, combining Zustand's core patterns with its ecosystem of middleware like Immer and Persist provides a highly productive developer experience. By adhering to best practices like using selectors and keeping stores domain-focused, you can ensure your application remains performant and maintainable as it grows.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles