---
title: How to use Zustand
description: We'll learn how to use Zustand in our React project.
slug: zustand-react-state
authors: chidume_nnamdi
tags: [dev-tools, react]
image: https://refine.ams3.cdn.digitaloceanspaces.com/blog/2023-05-18-zustand/social-3.png
hide_table_of_contents: false
---

**This article was last updated on July 31, 2024, to add sections for Advanced State Management Techniques and Custom Hooks for Zustand.**

## Introduction

Redux changed the game in the global management system. It was so successful that it was widely adopted and used as the ideal state management system in any framework. Not only in the framework but its principle still serves greatly in software development. Almost all developers have used Redux to manage their global state, and we can all attest to how powerful, fast, and maintainable it is to use Redux as the global state management tool. It makes debugging very easy and our app is predictable.

Yes, Redux took the grand stage in global state management, but there is a new kid on the block. This new kid is poised, and ready to capture the audience of the global state management system. It comes packing a powerful punch and is armed to the teeth. Its name is [Zustand](https://github.com/pmndrs/zustand).
With the arrival of Zustand and the goodies it brings, will it suffice to say that Redux reign is coming to an end?

In this article, we will deep dive into Zustand. We will learn about this kid, to understand how it works, its state management technique, and how it will take the state management world by storm.

Steps we'll cover:

- [What is Zustand?](#what-is-zustand)
- [Getting started with Zustand](#getting-started-with-zustand)
- [Build a To-do app using Zustand](#build-a-to-do-app-using-zustand)
- [Managing State Structures](#managing-state-structures)
- [Writing Custom Hooks](#writing-custom-hooks)

## What is Zustand?

> A small, fast, and scalable barebones state-management solution using simplified flux principles. Has a comfy API based on hooks, and isn't boilerplates or opinionated. [https://github.com/pmndrs/zustand](https://github.com/pmndrs/zustand)

Zustand is a small unopinionated state management library built by Jotai and React-spring. It has a comfy API based on hooks, and isn't opinionated. Zustand is open-source with a large community of users and support developers working round the clock to make Zustand stable. It sits on Github with 30,000+ stars.

Zustand is very different from Redux in terms of how it is used. Zustand is simple and un-opinionated, it does not wrap your app in content providers like how we do with React-Redux. It mainly uses hooks as a means of communicating back and forth with the state.
At its core, Zustand embraces the concept of a single source of truth, where the entire application state is stored in a centralized store. This store is composed of state slices, which are individual units of state that represent different parts of the application. Each state slice is defined as a separate store, allowing for modularity and encapsulation of related state properties and their associated actions.
Working with Zustand, you will need less boilerplate for it and its state management is centralized and action-based.

Zustand promotes immutability by default, ensuring that state updates are handled in an immutable fashion. When updating the state, you create a new state object rather than modifying the existing state directly. This approach simplifies state management, prevents common mutation-related bugs, and enables efficient change detection and re-rendering in React components.

Another notable feature of Zustand is its built-in support for subscriptions and selective reactivity. Components can subscribe to specific state slices and be automatically re-rendered when those slices change. Zustand uses a fine-grained dependency tracking mechanism based on proxies, allowing for highly efficient updates and minimizing unnecessary re-renders.

In the next section, we will learn how to install and use Zustand in our project.

## Getting started with Zustand

We know that Zustand is a JavaScript library that runs on Nodejs. So we will need some basic tools to be installed on your machine before we start.

- **Nodejs**: We need the Nodejs binaries installed in our system. Go over to [https://nodejs.org/en/download](https://nodejs.org/en/download) and install the binaries meant for your machine.
- **npm or yarn**: These are Node Package Managers, they help in maintaining and managing the dependencies, and the Nodejs environment of our project. `npm` comes bundled with the Nodejs binaries, so once you install Nodejs you don't need a separate installation for npm. [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/) can be installed by running `npm i yarn -g`.

Let's create a Nodejs project:

```
mkdir zustand-prj
cd zustand-prj
npm init --y
```

- `mkdir zustand-prj`: This command creates a new directory named "zustand-prj" in the current location. It is equivalent to "make directory." The "zu
- `cd zustand-prj`: This command changes the current working directory to "zustand-prj." By running this command, you will navigate into the newly created directory.
- `npm init --y`: This command initializes a new Node.js project within the "zustand-prj" directory. The npm init command is used to generate a package.json file, which is a manifest file that describes your project's metadata and dependencies. The `--y` flag is added to automatically accept all default options without prompting for user input. It is a shortcut for answering "yes" to all the initialization questions.

To install the zustand library, we run the below command:

```
npm install zustand # or yarn add zustand
```

This command installs the `zustand` library in our project.

To use zustand, we have to import a `create` function:

```tsx
import { create } from "zustand";
```

This function is called with a callback function and it returns a custom hook. The callback function passed to it is where we will define our state and the functions we can use to manipulate the state. The state and the functions are all in an object returned by this callback function.

Let's see an example:

```tsx
const useCounter = create((set) => {
  return {
    counter: 0,
    incrCounter: () => set((state) => ({ counter: state.counter + 1 })),
  };
});
```

See that the `create` function, passes a `set` function to the callback function. This `set` function is a function used to manipulate the state in the store. States in zustand can be primitives, objects, or functions. In our above example, we have two states in our store: `counter`, and `incrCounter`. The `useCounter` is a custom hook, we can use this hook in our components and we will be able to get the latest state in them. If we use the hook in components A, B, and C. Any change done to the state in B will be reflected in both A and C, and they will all re-render to reflect the new changes.

The custom hook returned by the `create` acts similarly to `useAppSelector` in React-Redux, it lets you select a slice of state from the store. You call the hook and pass it a callback function. This function is called by the hook internally and passes the current state to it. So we will then get this state and return the part of the state we want.

Let's see an example.

```tsx
const counter = useCounter((state) => state.counter);
```

See that we called the `useCounter` hook and passed a callback function to it. Then, we expect a state from the hook and then return the `counter` part of the state.

We can then, display the counter:

```tsx
const DisplayCounter = () => {
  const counter = useCounter((state) => state.counter);
  return <div>Counter: {counter}</div>;
};
```

Now, we want to create a component where we can increase the value of the `counter` state.

```tsx
const CounterControl = () => {
  const incrCounter = useCounter((state) => state.incrCounter);

  return (
    <div>
      <button onClick={incrCounter}>Incr. Counter</button>
    </div>
  );
};
```

This is a separate component from where we increase the value of the `counter` state. See that we sliced out the `incrState` function from the state, and we set it to the `onClick` event of the button. This will increase the `counter` state when the button is clicked.
See how the components are independent of each yet they can "see" the current state from the store. Whenever we click the `Incr. Counter` button in the `CounterControl` component, the `DisplayComponent` will re-render to display the newest `counter` state value.

Let's see how we use them:

```tsx
const App = () => {
  return (
    <>
      <DisplayCounter />
      <CounterControl />
    </>
  );
};
```

They are independent of each other yet magically connected by zustand. This gives React-Redux a run for its money because trying to re-create this small state in Redux-React will take more code to set up:

- First, we will create a store.
- We will wrap either the `App` component or its children in a Content Provider and pass the store to the Context Provider via a `store` props.
- We will import `useSelector, useDispatch` in any component we wish to use in the store.
- To get a slice of the state we will call the `useSelector` with a callback function.
- To dispatch an action to the store, we will use the `useDispatch` hook.

It's quite lengthy, but with Zustand it's oversimplified.

**Returning the whole state**
Now, when we call the custom hook returned by the `create` function without a callback function, the hook will return the whole state of the store.

```tsx
const state = useCounter();
```

See that we called the `useCounter` hook with no callback function, so in this case, the function will return the whole state in the store.

The `state` holds the whole state in the `useCounter` store. We can get the `counter` state by doing this:

```tsx
state.counter;
// 0
```

We can also, call the `incrCounter` state function:

```tsx
state.counter;
// 0

state.incrCounter();
// 1
```

**Memoization**
We can memoize our zustand store. Memoization is an optimization technique used to optimize the execution of functions by caching the results of expensive or time-consuming function calls. It involves storing the return value of a function associated with a specific set of input parameters so that if the function is called again with the same parameters, the cached result can be returned instead of re-evaluating the function.
The goal of memoization is to improve performance and efficiency by avoiding redundant computations.

Now, zustand gives us the ability to add memoization to the custom hook it returns to us. It exports a `shallow` function that we can use to add memoization to our state picks.

```tsx
import { shallow } from "zustand/shallow";
```

Still using our `useCounter` as an example, let's say we want to get the `counter` state from the store. We do this:

```tsx
// DisplayComponent
const counter = useCounter((state) => state.counter);
```

Now, let's say the initial state of the `counter` is 0, when the `counter` state is updated using the `incrCounter`, the `DisplayComponent` will be re-rendered. Now, if the updated value of the `counter` is 0 we will see that it is **unnecessary** to re-render the `DisplayCounter` component.

How do we stop this unnecessary re-rendering from occurring when the previous state and the next state are equal? Zustand directs us to pass a comparator function as a second parameter to the custom hook. This comparator function will compare the previous slice state and the next slice state, if both are the same the component will not re-render, else the component will re-render.

This is exactly what other React hooks do: useEffect, useMemo, and useCallback.
The `shallow` function is a comparator function provided to us by Zustand. It shallowly compares the two-state slices using the `==` shallow equality operator.

```tsx
const counter = useCounter((state) => state.counter, shallow);
```

See we passed the `shallow` function as a second parameter to the `useCounter` hook. On each state change in the store, the `shallow` will determine if the component will re-render based on the previous and next value of the `counter` state.
We can use our custom-made comparator if we don't trust the `shallow` function to do the job. The comparator function takes two parameters, the first parameter is the previous value of the state slice while the second parameter is the next value of the state slice.

```tsx
(previousState, nextStateSlice) =>
```

Inside this function is where we do our comparing and return the result. Returning `true` will make the hook skip the component from re-rendering while returning `false` will make the component re-render.

Let's create our comparator function for the `counter` state slice.

```tsx
(previousCounter, nextCounter) => previousCounter === nextCounter;
```

This uses the `===` equality operator to check if the two are the same. It returns a Boolean.

Let's plug it back into the `useCounter` hook:

```tsx
const counter = useCounter(
  (state) => state.counter,
  (previousCounter, nextCounter) => previousCounter === nextCounter,
);
```

Now, we have memoized our `useCounter` hook. With this, we have made our application a bit faster, with no more unnecessary re-renders.

**Updating the whole state**
We have only talked about getting the state from the store, but we have not delved into how to set the state. We only saw it briefly when we created the `useCounter` hook earlier on. Now, we will see how to update the state.

We learned that zustand passes a function to the callback function passed to the `create` function. This function widely accepted to be referred to as `set` is used to update all or parts of the state.

Let's look into the `incrCounter` state function:

```tsx
const useCounter = create((set) => {
  return {
    counter: 0,
    incrCounter: () => set((state) => ({ counter: state.counter + 1 })),
  };
});
```

Here, we are passing a callback function to the `set` function. The `set` function will call this function and pass it the state as an argument, then use the result of the function to update the state. See that in the callback function, we are returning an object with a `counter` property. The `set` function uses the properties found in the object to know the properties in the state to update.

We see that when we pass a function to the `set` function as an argument, the `set` function expects that we return an object.

We can pass an object to the `set` function:

```tsx
set({
  counter: 9,
});
```

This will update the `counter` state value to 9.

**Clear the entire state**
We can clear the state in a zustand store by passing an empty object to the `set` function.

```tsx
set({}, true);
```

This will clear the state and the actions.

**What are actions?**
Actions are functions that are part of the state in a zustand store. They are like dispatch actions in React-Redux, they are used to effect changes in the store. For example, our `incrCounter` is an action, we call the `set` function inside it to update the `counter` state.

**Using async actions**
Actions in zustand also support asynchronicity. In fact, according to Zustand docs, zustand does not care if your action is asynchronous or not.
We can perform an asynchronous function in an action. For example, we can make an HTTP request to an endpoint from our action and update the state with the result from the HTTP call.

Let's show an example:

```tsx
const useCounter = create((set) => {
  return {
    counter: 0,
    incrCounter: async () => {
      const { data } = await axios.get("/counter");
      set({
        counter: data.counter,
      });
    },
  };
});
```

See that in the `incrCounter` we made it an asynchronous function by using the `async` keyword. Inside the function, we made a call to an `/counter` endpoint and use the `set` function to update the value of the `counter` in the state.

## Build a To-do app using Zustand

Now, we have learned the basics of zustand and its API. We will create a To-do app using Zustand.

The To-do app will be a React app, while the Zustand will power our state management.

Let's start, we will scaffold a React project using the `create-react-app` tool:

```tsx
create-react-app todo-app
cd todo-app
```

Next, we install `zustand`:

```
npm install zustand
```

The first thing we create our hook store:

```tsx
import create from "zustand";

const useStore = create((set) => ({
  todos: [],
  addTodo: (text) =>
    set((state) => ({
      todos: [
        ...state.todos,
        {
          id: Date.now(),
          text,
          completed: false,
        },
      ],
    })),
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    })),
  deleteTodo: (id) =>
    set((state) => ({
      todos: state.todos.filter((todo) => todo.id !== id),
    })),
}));

export default useStore;
```

See, we have a `todos` state. This will hold an array of our todos. We have three actions: `addTodo`, `toggleTodo`, and `deleteTodo`. The `addTodo` action adds new todo to the `todos` state. , the `toggleTodo` action toggles the `completed` state of a todo. The `deleteTodo` removes a todo from the array state.
Now, let's build the components.

**DisplayTodos**
This component will have one job. It will render the `todos` state:

```tsx
const DisplayTodos = () => {
  const { todos, deleteTodo } = useStore((state) => {
    return { todos: state.todos, deleteTodo: state.deleteTodo };
  });

  return (
    <ul>
      {todos.map((todo) => (
        <li
          key={todo.id}
          style={{
            textDecoration: todo.completed ? "line-through" : "none",
          }}
          onClick={() => toggleTodo(todo.id)}
        >
          {todo.text}
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
};

export default DisplayTodos;
```

We sliced off the `todos` array from the state and used the `Array#map` method to render the todos, also we sliced off the `deleteAction`. The `Delete` button removes each todo from the list, it does this by calling the `deleteAction` action with the id of the todo clicked.
Now, let's build the component where we can add a todo to the list.

**TodosControl**

```tsx
const TodosControl = () => {
  const addTodo = useStore((state) => state.addTodo);
  const [text, setText] = useState("");

  function handleSubmit(e) {
    e.preventDefault();
    addTodo(text);
    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default TodosControl;
```

This component provides us a form where we input a todo and add it to the state store. We have a state `text` that holds the text we type in the input element. Then, the `handleSubmit` function is called when the form is submitted via the `Add` button. Inside the `handleSubmit` function, the `addTodo` is called passing in the text in the `text` state as an argument. This will create and add a new todo to the `todos` state.

Bringing them all together:

```tsx
const App = () => {
  return (
    <>
      <DisplayTodos />
      <TodosControl />
    </>
  );
};

export default App;
```

## Managing State Structures

I was very eager to present advanced patterns of state management with Zustand, which I've come to investigate recently. These will help us deal with more complex state structures and improve application performance and maintainability.

Zustand helps us create nested state slices, so handling complex state structures is straightforward. Structuring the state in a more modular way makes it easier to update and manage some specific bits of state without impacting others.

Here is an example:

```javascript
const useStore = create((set) => ({
  user: {
    name: "",
    age: 0,
    address: {
      street: "",
      city: "",
    },
  },
  updateUser: (newUser) =>
    set((state) => ({ user: { ...state.user, ...newUser } })),
  updateAddress: (newAddress) =>
    set((state) => ({
      user: {
        ...state.user,
        address: { ...state.user.address, ...newAddress },
      },
    })),
}));
```

In this case, we had a nested state for the user and address, and therefore separate methods to update the fields of the user and address, respectively.

### Use Middleware with Zustand

Zustand can be enhanced with middleware to provide additional functionality, such as changes in state logging, the ability to save and load states in local storage, and handling asynchronous actions.

Below is how we may use middleware to log state changes:

```javascript
import { create } from "zustand";
import { devtools } from "zustand/middleware";

const useStore = create(
  devtools((set) => ({
    count: 0,
    increment: () => set((state) => ({ count: state.count + 1 })),
  })),
);
```

Wrapping our store definition with the `devtools` configuration now allows state changes to be observable in the Redux DevTools extension, which is a great tool for debugging.

## Writing Custom Hooks

We'll see how to create custom hooks and utilities that would make state management easy with Zustand. They help in the reuse of logic for cleaner code in our React projects.

Custom hooks can encapsulate Zustand state logic, making it a lot easier to reuse and manage complex state interactions. Here's an example of how one can do that for managing user authentication state:

```javascript
import { create } from "zustand";

const useAuthStore = create((set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
}));

const useAuth = () => {
  const { user, login, logout } = useAuthStore();
  return { user, login, logout };
};

export default useAuth;
```

With this custom hook, you may use it to manage the authentication state in any component by just calling `useAuth`.

### Constructing Utility Functions

Utility functions can also be used to simplify the state update and make code readability much easier. Below, take a look at an example with an updated nested state:

```javascript
const updateNestedState = (set, keyPath, value) => {
  set((state) => {
    const keys = keyPath.split(".");
    let nestedState = state;
    keys.slice(0, -1).forEach((key) => {
      nestedState = nestedState[key];
    });
    nestedState[keys[keys.length - 1]] = value;
    return { ...state };
  });
};

const useNestedStateStore = create((set) => ({
  data: {
    user: {
      profile: {
        name: "",
      },
    },
  },
  updateProfileName: (name) =>
    updateNestedState(set, "data.user.profile.name", name),
}));
```

That makes our task easier in updating state properties that are deeply nested and allows the use of the utility function in various parts of our application.

## Conclusion

We have come a long way. Zustand is interesting and freakingly minimal and basic to use. Mind you, what we learned here is just the basics, the power of Zustand is yet to be covered in total and it runs deep.

Let's recap. We started by introducing Zustand; how it works and how it differentiates itself from the world-popular Redux. Next, we saw how to install the Zustand library, we learned also how to set up a state in it and how to use the actions. We learned how to set up async operations in actions, how to update a state, and how to get slices off a state.
