When talking about the global state of an application, we refer to the data that needs to be accessible in multiple components, regardless of their location in the component tree.
For years, global state management in React had one name: Redux. Redux was powerful, but absurdly complex. It required writing a huge amount of repetitive code to do simple things.
Then came the Context API, which is easy to use but has performance problems if you’re not careful.
Then came ❤️ Zustand to solve everything. A library that removes complexity, eliminates <Provider> wrappers, making managing global state as easy as declaring a variable.
Why Zustand?
Zustand (meaning “State” in German) proposes a minimalist philosophy:
- No providers: You don’t need to wrap your
<App>with anything - Less code: Defining a store takes just a few lines
- Performance: Components only re-render if exactly the data they are listening to changes
Installing Zustand
To start using Zustand, we first need to install it in our project:
npm install zustand
Once installed, we can start using it. It’s that easy.
Creating a Store with Zustand
In Zustand, the state lives in a concept called a Store. A store is simply an object that contains:
- The values (state)
- The functions to update them (actions).
For example, let’s see how to create a Store to manage a shopping cart (to avoid making another counter).
Create the file src/store/useCartStore.js.
import { create } from 'zustand';
// create receives a callback function that gives us the 'set' method
export const useCartStore = create((set) => ({
// 1. Initial state (Variables)
items: 0,
totalPrice: 0,
// 2. Actions (Functions to modify the state)
addItem: () => set((state) => ({
items: state.items + 1
})),
increasePrice: (amount) => set((state) => ({
totalPrice: state.totalPrice + amount
})),
removeAll: () => set({ items: 0, totalPrice: 0 }),
}));
Notice the set function. Unlike useState, here you don’t replace the entire state, but rather it gets merged.
If we do set({ items: 0 }), the totalPrice property remains untouched. There’s no need to copy it manually with ...state.
Consuming State in Components
To use the state, we simply import our useCartStore hook.
import { useCartStore } from '../store/useCartStore';
function BadgeCarrito() {
// We only subscribe to 'items'.
// If the price changes, this component does NOT re-render
const items = useCartStore((state) => state.items);
return <span className="badge">{items}</span>;
}
To use the actions (which are functions and never change), we can extract them the same way:
function BotonComprar() {
const addItem = useCartStore((state) => state.addItem);
return <button onClick={addItem}>Add to Cart</button>;
}
We could also have done this, but in this case, if totalPrice changes, the component would re-render. That’s why it’s better to do it the other way.
// ⚠️ If 'totalPrice' changes, this component renders anyway
const { items, addItem } = useCartStore();
Data Persistence in LocalStorage
Do you want the shopping cart not to be lost if the user reloads the page? Zustand comes with a middleware called persist that automatically saves your state to localStorage (or sessionStorage) and hydrates it on startup.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export const useCartStore = create(
persist(
(set) => ({
items: 0,
addItem: () => set((state) => ({ items: state.items + 1 })),
}),
{
name: 'cart-storage', // Key name in localStorage
}
)
);
Just by adding that wrapper, your state becomes persistent. Without writing a single line of localStorage.setItem.
DevTools
If you miss the Redux browser extension to see how the state changes over time, good news! Zustand is compatible with Redux DevTools via another middleware.
import { devtools } from 'zustand/middleware';
const useStore = create(devtools((set) => ({ ... })));
Unless you are working on a legacy project that uses Redux, or on a massive enterprise application with very specific data flow requirements, Zustand should be your default choice.
