Skip to content
OVEX TECH
Education & E-Learning

Master Zustand: Build Better React Apps Easily

Master Zustand: Build Better React Apps Easily

Build Better React Apps with Zustand

Zustand is a powerful tool for managing your application’s data, making your code cleaner and your apps faster. This guide will show you how to use Zustand to replace complex state management solutions like React Context.

You’ll learn to create stores, update state, and optimize your application’s performance. By the end, you’ll be able to build scalable real-world applications with Zustand.

What You’ll Learn

This tutorial covers the following:

  • Setting up your first Zustand store.
  • Comparing Zustand with React Context to see performance benefits.
  • Optimizing component re-renders with Zustand selectors.
  • Using middleware like persist and immer for advanced features.
  • Managing state both inside and outside React components.

Prerequisites

  • Basic understanding of React and JavaScript.
  • Familiarity with React Hooks (like useState, useContext).
  • Node.js and npm/yarn installed for package management.

Step 1: Understand the Problem with React Context

Many React applications use Context for state management. While useful, Context can cause performance issues.

When any part of the context changes, all components using that context might re-render, even if they don’t use the changed data. This can slow down your app, especially as it grows larger and more complex.

Imagine a shared notebook where every time someone writes a new sentence, everyone reading the notebook has to re-read the whole thing. This is similar to how Context can work, causing unnecessary work for your application.

Step 2: Install Zustand

First, you need to add Zustand to your project. Open your terminal in your project’s root directory and run the following command:

npm install zustand

This command downloads and installs the Zustand library, making its features available for your use.

Step 3: Create Your First Zustand Store

Zustand works by creating custom hooks that act as your state containers, called stores. Create a new file, for example, useCounterStore.ts, inside a store folder.

Inside this file, you’ll use the create function from Zustand. This function takes another function as an argument. This inner function receives a set function to update the state and should return an object representing your store’s initial state and actions.

Here’s how to create a simple counter store:


import { create } from 'zustand';

type State = {
 count: number;
 increment: () => void;
 decrement: () => void;
 reset: () => void;
};

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

In this code, we define the types for our state and actions. The create function initializes the count to 0 and defines the increment, decrement, and reset functions. Notice how set is used to update the state; it works similarly to React’s setState.

Step 4: Use Your Zustand Store in Components

Now, replace your Context usage with the Zustand store. In your React component, import and call your custom hook (useCounterStore).

You can then destructure the state and actions you need directly from the hook’s return value. This is much simpler than using useContext.

For example, to display the count:


import { useCounterStore } from './store/useCounterStore';

function CountDisplay() {
 const count = useCounterStore((state) => state.count);
 return 

Count: {count}

; }

And for the controls:


import { useCounterStore } from './store/useCounterStore';

function CounterControls() {
 const increment = useCounterStore((state) => state.increment);
 const decrement = useCounterStore((state) => state.decrement);
 const reset = useCounterStore((state) => state.reset);

 return (
 
); }

By selecting only the specific pieces of state you need (like count or increment), you ensure that components only re-render when that particular data changes. This is a key performance advantage over Context.

Step 5: Optimize Re-renders with Selectors

To get the performance benefits, you must select only the data your component needs. If you call useCounterStore() without any arguments, it returns the entire state object. This will cause re-renders whenever any part of the state changes, similar to Context.

Instead, pass a selector function to the store hook. This function receives the state and returns only the specific values you want. For example, useCounterStore((state) => state.count) tells Zustand to only update this component when the count changes.

When selecting multiple values that might be returned as an object, use the useShallow hook from Zustand. This performs a shallow comparison, preventing unnecessary re-renders if the object reference changes but its contents do not.


import { useCounterStore } from './store/useCounterStore';
import { useShallow } from 'zustand/react';

function CounterDisplay() {
 const count = useCounterStore(useShallow((state) => state.count)); // Use useShallow if selecting multiple primitives or objects
 return 

Count: {count}

; } function CounterControls() { const { increment, decrement, reset } = useCounterStore(useShallow((state) => ({ increment: state.increment, decrement: state.decrement, reset: state.reset, }))); return (
); }

Using selectors correctly ensures that your components are efficient and only update when necessary.

Step 6: Access State Outside React Components

A major advantage of Zustand is that you can access and modify your state from anywhere, not just within React components. This is useful for utility functions or background tasks.

You can call useCounterStore.getState() to get the current state or useCounterStore.setState(...) to update it directly, without needing to be inside a component.

For example, to increment the count from a non-React function:


import { useCounterStore } from './store/useCounterStore';

function handleExternalIncrement() {
 useCounterStore.getState().increment(); // Or useCounterStore.setState(state => ({ count: state.count + 1 }));
}

This flexibility makes Zustand incredibly powerful for managing complex application states.

Step 7: Persist State with Middleware

Zustand offers middleware to add extra functionality. A common use case is persisting state to localStorage so it survives page reloads.

Use the persist middleware by wrapping your store creation. You’ll need to install it if it’s not included by default and configure a name for your stored data.

First, install persist if needed (often included):

npm install zustand @pmndrs/pmc --save-dev (or similar, check docs)

Then, wrap your store creation:


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

type State = {
 count: number;
 increment: () => void;
 decrement: () => void;
 reset: () => void;
};

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

export const useCounterStore = create(persist(() => store.getState(), {
 name: 'counter-storage', // name of the item in the storage (must be unique)
}));

Now, your counter’s state will be saved in localStorage under the key ‘counter-storage’ and automatically loaded when the app starts.

Step 8: Manage Nested State with Immer

Updating nested state directly can be cumbersome, requiring manual merging of objects. Zustand integrates well with immer to simplify this.

Install immer:

npm install immer

Then, wrap your store creation with immer middleware. This allows you to directly mutate state within your actions, and Immer handles the immutability behind the scenes.


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

type State = {
 user: {
 name: string;
 address: {
 street: string;
 zipCode: string;
 };
 };
 updateStreet: (newStreet: string) => void;
};

export const useUserStore = create()(
 immer((set) => ({
 user: {
 name: 'John Doe',
 address: {
 street: 'Main St',
 zipCode: '12345',
 },
 },
 updateStreet: (newStreet) => {
 set((state) => {
 state.user.address.street = newStreet;
 });
 },
 }))
);

With Immer, you can write code like state.user.address.street = newStreet, which is much cleaner than manually creating new objects for each level of nesting.

Conclusion

Zustand provides a simple yet powerful way to manage state in your React applications. By understanding stores, selectors, and middleware, you can build performant and scalable applications. Start implementing these techniques in your projects today to simplify your state management.


Source: Zustand Crash Course (YouTube)

Leave a Reply

Your email address will not be published. Required fields are marked *

Written by

John Digweed

3,140 articles

Life-long learner.