Zustand React State Management - Simpler Alternative to Redux
Zustand React State Management - Simpler Alternative to Redux

In this post we will talk about such state management for React which is called Zustand and this is an alternative to Redux.

Inside React out of the box we don't have a nice state management system. Yes we have useState or useReducer but they are both local to the component.We can use React.Context to make it globally available but this is not some library or framework that we can follow. We are completely on our own.

This is why typically 90% of the people will just take Redux as a scalable framework which is used in lots of companies. This is a really solid choice and this is what I prefer for production.

But some people don't like the amount of knowledge that you need in order to master Redux. You need to understand actions, middlewares, reducers, async actions and much more. This is why a lot of people want Redux but which is easier to learn. Zustand is exactly one of such libraries.

Zustand is small and scalable state management solution using flux principles. It is based on hooks and you should not write lots of boilerplate.

I completely agree with this line. It is much easier to start with Zustand than with Redux.

Moving to Zustand

But enough talking let's just convert Redux project to Zustand to see what are the difference. First of all let's look on our project.

Our project

We fetch data from API and store them in our state with the help of Redux. Now we want to try and refactor this code to Zustand and see the difference.

Our first step is to install Zustand library.

yarn add zustand

In src folder I want to add stores folder where we will write our Zustand states.

// src/stores/useUsersStore.js
import create from "zustand";

export const useUsersStore = create((set, get) => ({
  data: [],
  isLoading: false,
  error: null,
}));

Here we created a file useUsersStore inside we created our store for users by using create function from Zustand. As you can see we return an object with all data that we need in our state. Exactly in the same way like we create state in Redux.

export const useUsersStore = create((set, get) => ({
  ...
  getUsers: async () => {
    try {
      set({ isLoading: true });
      const response = await getUsers();
      set({ isLoading: false, data: response.data });
    } catch (err) {
      set({ error: err.message, isLoading: false });
    }
  },
}));

Now we directly created an async function getUsers inside our object. As you can see we don't need actions or helpers to write async code like we need in Redux. It is either just an async or sync function.

Inside our getUsers we use set function which we got as a parameter inside create. It allows us to define how we change a state inside our store. So here we defined what should happen on success and on failure when we call our API.

Now let's apply the same logic to createUser.

export const useUsersStore = create((set, get) => ({
  ...
  createUser: async (name) => {
    try {
      set({ isLoading: true });
      const response = await createUser(name);
      const updatedData = [...get().data, response.data];
      set({ isLoading: false, data: updatedData });
      // set((state) => ({
      //   isLoading: false,
      //   data: [...state.data, response.data],
      // }));
    } catch (err) {
      set({ error: err.message, isLoading: false });
    }
  },
}));

As you can see it is super similar. The only difference is that we used get function to access current state. In the commented code I showed a way to access the current state directly inside set but it can make your code less readable.

Inside Zustand we can define state and functions in a single object. We can also create multiple stores in comparison to Redux.

Now let's update our component and use Zustand instead of Redux.

import { useState, useEffect } from "react";
import { useUsersStore } from "./stores/useUsersStore";

const Users = () => {
  const users = useUsersStore((state) => state.data);
  const getUsers = useUsersStore((state) => state.getUsers);
  const createUser = useUsersStore((state) => state.createUser);
  const [inputValue, setInputValue] = useState("");
  const addUser = () => {
    createUser(inputValue);
    setInputValue("");
  };

  useEffect(() => {
    getUsers();
  }, [getUsers]);

  return (
    <div>
      <div>Total: {users.length}</div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={addUser}>Add user</button>
      </div>
      <div>
        {users.map((user, index) => (
          <div key={index}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};
export default Users;

Here we used useUsersStore to access specific property in our component. We don't access the whole object but each specific property as it is much better for performance. Also we access our actions like getUsers and createUser in exactly the same way like properties.

All properties and functions are available for us without wrapping them in dispatch like we must do in Redux which simplifies our code.

Verdict

What I really like about Zustand is that it is much easier for people to start than with Redux. It is also much easier to learn and you write much less code than with Redux.

But here are also some points that I don't like. Documentation could be better and in comparison to Redux it is lacking. Secondly with usage of lots of stores you might have problems to sync data between then later. Redux single object architecture was developer after flux multiple stores for a reason.

Most importantly Zustand is not that strict in comparison to Redux. Which actually means that you can write things like you want, you don't have one global store which really makes it more difficult to scale you application.

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done