DEV Community

Cover image for Manage the state of your React app with Zustand
Francisco Mendes
Francisco Mendes

Posted on • Updated on

Manage the state of your React app with Zustand

React State management has evolved a lot in recent years and this same evolution brought several options, which despite solving exactly the same problem, each of them brings its own approach.

What is a state manager?

Even though your application does not appear to be very complex, you should know that doing many simple things requires a lot of manual data manipulation, which consequently leads to duplication and mutation of data. Obviously it then leads to an exhaustive work on solving problems and bugs.

The state manager acts as a single source of truth for your application. It defines a way in which we can store data, modify it and react to its changes.

What are the options?

Speaking now of React, there are several strategies and the favorite is the use of the famous hook useState. In many contexts this is a great option, if you just want to handle the data in the component in question or if you want to pass the state as props.

But once you have a more complex project you will face several challenges, and in my opinion, the biggest challenge will be the hierarchy of the components of your application.

The fundamental idea is to have a parent component (with state) that delegates to child components (stateless). This is an easy strategy to understand and to implement, because as soon as there is a change in the parent component, the change will be immediately visible in the child components. However, if we want to share data between several child components without first interacting with the parent component, this strategy becomes useless.

Global State strategy

If you don't want to use third-party libraries, you can use React's Context API without any problems. This way you can share data within the application without having to pass props between the components. Just iterate with the state manager store, which, as already mentioned in this article, becomes a single source of truth for our application.

In my opinion, the only negative point of the Context API is exactly the same as Redux, we need a huge boilerplate in order to have a fully functional global store. Despite being an asset for large applications, for small applications it can bring a whole complexity. Because in addition to being concerned with the life cycle of our React components, we also have to be concerned with the life cycle of our store.

With the last paragraph I did not want to denigrate the quality of both Context API and Redux, the intention was to mention that we must use certain libraries according to the context. Sometimes they are good investments but in other cases they can end up giving us headaches.

What is zustand?

Zustand is one of the simplest and lightest open source state management libraries. Although it is simple, you can work with large-scale applications because it is very flexible.

What I like about this library is the fact that it is an external store with which we can interact using hooks. The approach is very similar to the state managers previously mentioned in this article, the difference is the amount of boilerplate that we have to write.

To demonstrate the simplicity of zustand I had the idea to create a very simple react application, in which we will have two views. The Home page, where we will have a form where we have to fill in our name and submit the value of the input to our global store. And the Details page, where we will display the name that is stored in our store.

Let's code

The dependencies necessary for the development of the example in this article were as follows:

npm install react-router-dom zustand
Enter fullscreen mode Exit fullscreen mode

In my App.jsx I imported all the necessary components from react-router-dom to create my routes and assigned each one of them their own views.

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'

import { Home, Details } from './pages'

export default function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/details" component={Details} />
      </Switch>
    </Router>
  );
}
Enter fullscreen mode Exit fullscreen mode

And this is the code for the Home page view:

import { useState } from "react";
import { useHistory } from "react-router-dom";

export default function Home () {
  const [form, setForm] = useState("");
  const history = useHistory();
  const handleOnSubmit = (e) => {
    e.preventDefault();
    history.push("/details");
  };
  return (
    <form onSubmit={handleOnSubmit}>
      <input
        type="text"
        placeholder="Type your name."
        value={form}
        onChange={(e) => setForm(e.target.value)}
        required
      />
      <button type="submit">Submit</button>
    </form>
  );
};
Enter fullscreen mode Exit fullscreen mode

And this is the code for the Details page view:

import { useHistory } from "react-router-dom";

export default function Details () {
  const history = useHistory();
  const handleOnClick = (e) => {
    e.preventDefault();
    history.push("/");
  };
  return (
    <>
      <h1>Your name is: {/* Current name goes here */}</h1>
      <button onClick={handleOnClick}>Clear</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, the only form of state management I am using is useState. But as we are going to create a global store, we are now going to create our store.js at the root of our project.

First, we import zustand by giving it the name create. Then we will create a constant called useStore that will basically be our React Hook to interact with our store.

import create from 'zustand'

export const useStore = create(set => ({
  // Logic goes here
}))
Enter fullscreen mode Exit fullscreen mode

This Hook we just created has two important elements. The first element is the initial state and the second one is a function that is used for updating the state (action).

Let's name our initial state currentName and setCurrentName our action name. Like other state managers, states are meant to be read-only while actions are used to make mutations.

So the final code of our store should be as follows:

import create from 'zustand'

export const useStore = create(set => ({
    currentName: '',
    setCurrentName: (currentName) => set({ currentName }),
}))
Enter fullscreen mode Exit fullscreen mode

Now with our global store completed, we will import our useStore Hook on the Home page, then we will get our setCurrentName action from our store and finally we pass the state of our form to our action, so that the data goes to our global store.

// Hidden for simplicity

import { useStore } from "../store";

export default function Home () {
  const { setCurrentName } = useStore();
  const [form, setForm] = useState("");
  // Hidden for simplicity
  const handleOnSubmit = (e) => {
    e.preventDefault();
    setCurrentName(form);
    history.push("/details");
  };
  return (
    // Hidden for simplicity
  );
};
Enter fullscreen mode Exit fullscreen mode

Finally, we import our Hook again, but this time on the Details page. Then we will get our currentName state and we will get our setCurrentName action again. Our currentName will be exclusively to read the value of our state, while setCurrentName will be to reset it.

It should be like this:

// Hidden for simplicity

import { useStore } from "../store";

export default function Details () {
  const currentName = useStore((state) => state.currentName);
  const { setCurrentName } = useStore();
  // Hidden for simplicity
  const handleOnClick = (e) => {
    e.preventDefault();
    setCurrentName("");
    history.push("/");
  };
  return (
    <>
      <h1>Your name is: {currentName}</h1>
      <button onClick={handleOnClick}>Clear</button>
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

Explanation:

In our global store we have two elements, the state and the action. On the Home page, we only use the action because the only thing we need to do is assign the value of the form's input to our store as soon as the button is clicked.

While on the Details page we need to do two things. The first is to acquire the current value of the state that we have in our global store so that it is printed on our application. The second is to get our action again so that we can reset the value of our state in the global store (as soon as the button is clicked).

application

Redux devtools

With zustand you can also enjoy the redux devtools extension and all that is needed is to import it from zustand/middleware and pass our store as the first argument.

Like this:

import create from 'zustand'
import { devtools } from 'zustand/middleware'

export const useStore = create(devtools(set => ({
    currentName: '',
    setCurrentName: (currentName) => set({ currentName }),
})))
Enter fullscreen mode Exit fullscreen mode

Persist state

If you want to persist the state of your application using zustand, you can do it in exactly the same way as redux devtools.

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

export const useStore = create(persist(set => ({
    currentName: '',
    setCurrentName: (currentName) => set({ currentName }),
})))
Enter fullscreen mode Exit fullscreen mode

Final notes

The example I presented here about zustand is really the simplest implementation that can be done, but in my opinion it is the easiest way to start integrating it into projects. However, I repeat that even though zustand has such a simple and friendly API, it is a strong competitor for other state managers that are better established in the market.

And once again giving my opinion, I think that zustand is a technology that can be implemented early on in our applications due to the fact that with little boilerplate it offers so much out of the box. And as our application grows, zustand is flexible enough to increase the complexity of our store. Perhaps it is a unique feature compared to its competitors.

What about you?

What form of state management do you use?

Top comments (2)

Collapse
 
rolfikv profile image
RolfK

great post!! thanks man!

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thanks 👊