Marcio Cunha

How to manage global state in React with Zustand easily

Learn how to handle global state in React cleanly, fast, and without boilerplate code by using Zustand instead of Redux.

Marcio Cunha•5 min
Also available in:EspañolPortuguês
Summary
  • Zustand eliminates the classic verbosity of reducers and actions by adopting a pure hooks-based API
  • Built-in state selectivity prevents unnecessary re-renders in components that haven't changed
  • Managing data outside the React component ecosystem makes unit testing and async logic much easier
  • Optimizing web bundles, as detailed when applying code splitting in React, enhances overall app delivery
  • The minimalist approach replaces complex tools without sacrificing scalability in large applications

The challenge of sharing data between components in React

When building modern web applications, we soon realize that not every piece of information belongs in a single corner of the screen. Users log in, toggle interface themes, or add items to a shopping cart, and these data points must appear instantly across multiple distinct places. In practice, managing this ecosystem requires sharing information globally without turning the codebase into an unmaintainable tangle. This is where global state comes in—a central warehouse where we store everything that various parts of the system need to view and modify.

Historically, the community relied on robust tools like Redux to solve this equation. Although extremely powerful, Redux demands a massive amount of repetitive code, commonly known as boilerplate. To create a simple data modification, you had to configure actions, reducers, types, and complex middlewares before seeing anything work on screen. For smaller teams or projects seeking agility, this bureaucracy penalizes productivity and makes the learning curve unnecessarily steep. As we saw when discussing heavy bundle in React and how to apply code splitting, keeping code clean directly impacts performance and maintainability.

Discovering Zustand and its minimalist approach

Zustand arrives as a breath of fresh air in this scenario. In German, the name literally means state. It is a minimalist library created by the same developers behind Zustand and Jotai, focused on delivering maximum power with the absolute minimum amount of code. It utilizes native React hooks directly, discarding complex context providers at the root of your application and completely eliminating the need for rigid reducers.

In practice, this means creating a global data hub boils down to writing just a few lines of modern JavaScript. You define a function that stores both the variables and the functions responsible for modifying them, and any component can consume that data instantly. There are no deep architectural secrets or abstract concepts to memorize on your first day of use. This simplicity allows both junior and senior developers to reach the same level of efficiency within minutes of reading.

Building your first effortless data store

To understand how the tool works on the workbench, let's build a practical example of managing a simple shopping cart. The first step is to install the library in your project using your preferred package manager, such as npm or yarn, ensuring the environment is ready to receive the new dependency.

Next, we create a dedicated file for our storage, which we will call the store. Inside it, we define the data structure and permitted actions. Notice how direct and clean the syntax is, very reminiscent of creating a standard custom hook:

import { create } from 'zustand';

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

With just a few lines, we structured the complete data foundation. The set function serves precisely to update the state immutably, ensuring React notices the change and updates the visual interface. There are no giant switch-cases or files scattered across distant folders.

Consuming state directly inside components

The true beauty of Zustand appears when we consume this information inside visual components. Because the library integrates natively with React hooks, you don't need to wrap your entire application in bulky provider components. Simply import the created hook and extract the exact piece of information your component needs to render on screen.

This approach brings a colossal performance boost. In traditional global state solutions, any minor change in a remote variable could trigger the re-rendering of entire components that weren't even using that specific data. With Zustand, the component subscribes only to the slice of state it cares about, avoiding wasted processing in the user's browser.

import React from 'react';
import { useCartStore } from './cartStore';

export function CartCounter() {
  const items = useCartStore((state) => state.items);
  
  return (
    <div className='p-4 bg-gray-100 rounded'>
      <p>Items in cart: {items.length}</p>
    <>
  );
}

In the example above, the CartCounter component watches exclusively the list of items. If another part of the application modifies the visual theme or the user profile, this component remains untouched, saving precious rendering cycles and keeping the interface extremely fluid and responsive.

Handling asynchronous requests and APIs

Real applications need to talk to external servers to fetch data, authenticate users, and save information in databases. One of Zustand's great advantages is that it handles asynchronous operations completely naturally, without requiring additional middlewares like Redux Thunk or Redux Saga. Since storage actions are just standard JavaScript functions, you can use async/await with zero barriers.

To fetch data from an API, simply declare an asynchronous function inside your store and update the state as soon as the response arrives. This drastically simplifies backend communication architecture, allowing business logic to stay concentrated right where the data resides, facilitating automated testing and production error debugging.

export const useUserStore = create((set) => ({
  user: null,
  loading: false,
  fetchUser: async (id) => {
    set({ loading: true });
    const response = await fetch(`https://api.example.com/users/${id}`);
    const data = await response.json();
    set({ user: data, loading: false });
  },
}));

Thus, the asynchronous data flow becomes transparent and straight to the point. Any screen in the application can trigger the fetch function simply by calling the hook and consuming the loading indicator or the already formatted user data. To complement this implementation in production environments, see our related technical analyses on How to use local embeddings with ChromaDB for semantic search and How to manage concurrency in Rust with Tokio and Channels.

Final thoughts on frontend architecture

Adopting a minimalist tool changes how we approach building complex web applications. By removing excessive layers of abstraction and focusing on what truly matters—code readability and interface performance—we gain delivery speed and reduce long-term technical debt. Zustand proves we don't need complex frameworks to solve everyday global state problems.

Evaluating the current React ecosystem landscape, lightweight tools focused on developer experience are here to stay. Replacing legacy, heavy patterns with agile alternatives allows teams to focus on real end-user experience. Choosing the right technology to manage your data ensures the application remains scalable, clean, and easy to maintain as the project grows over the years.