Using Zustand to Manage State in React App
Tags: react, javascript, webdev, tutorial
Published: 2022-09-07 13:15:09

Building modern web applications often implies our application is made up of several components, which we need to keep in sync by sharing some state between them. State management libraries like Zustand, provide an intuitive way to share state through out our applications, by providing a centralized store for our state ,solving issues like prop drilling. This article aims to guide the user on how to get started with Zustand in their react application. 

## What is Zustand

Zustand is a small, fast and scalable state management solution that uses simplified flux principles, is based on hooks and has little to no boilerplate code. Having about **20.6k** stars on github, Zustand is becoming one of the most loved state management solutions thanks to its simplicity.

**- small**
Zustand is one of the smallest state management libraries with a bundle size of just **1.16kb**.

 **- fast**
Zustand is fast, thanks to its small size and little boilerplate code, making us have instantaneous UI updates on changes to our state.

**- scalable**
With Zustand, we can create a store, which is a central point of truth where all our global app state resides in. Our store is simply a hook, which we can easily import and use where needed. making Zustand very scalable.

Enough talk, lets get out hands dirty by building a simple todo application and use Zustand to manage state, so you get to see how things work

![interesting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1btjg3vl1exr2gv9b7j.gif)

## Getting started with React and Zustand
Setting up Zustand in our react app is as easy as setting up our react app with any other npm dependency. To get things started, run the following command

```
npx create-react-app todo-app
cd todo-app
npm install zustand
```

or if you already have an existing react app, simply run

```
npm install zustand
yarn add zustand
```

Now that our state management library is all setup in our react app, we can start working with it. My _App.js_ file is very simple 

```
function App() {

  const inputRef = useRef();

  return (
    <div>
      <div style={{ justifyContent: 'center', display: 'flex', padding: '15px' }}>
        <input type={'text'} placeholder='enter user name' style={{ padding: '10px,15px' }} ref={inputRef} />
        <button >Add</button>
        <button >fetch</button>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px', borderTop: '1px solid black' }}>
        <ul style={{ textAlign: 'center', }}>
        </ul>
      </div>
    </div>
  );
}

export default App;
```
A Very simple UI for our todo app. An input box, 2 buttons, and an unordered list to render todo elements.

## Creating and Accessing our Store

- **creating store**

In our _src_ folder, we create a _store.js_ file which will contain our global state. Our _store.js_ file looks like this

```
import create from 'zustand';

let store = () => ({
    users: [{ id: 1, name: 'Teyim Asobo' }, { id: 2, name: 'Fru Brian' }],
})

export const useStore = create(store);
```
Here, we have created a simple store using the **create** function imported from Zustand. Our store is a hook (**useStore**), and can be accessed any where in our application via a simple import. Our store currently contains an array of users, with 2 users initialiased.

Now that our store is created, lets try to access our state from our _App.js_ component.

- **Accessing store**

We can bind our components to our state, by calling our hooks from anywhere in our applications

```

import { useStore } from './store'
import { useRef } from 'react';

function App() {
  const inputRef = useRef();
  const users = useStore(state => state.users)
  return (
    <div>
      <div style={{ justifyContent: 'center', display: 'flex', padding: '15px' }}>
        <input type={'text'} placeholder='enter user name' style={{ padding: '10px,15px' }} ref={inputRef} />
        <button >Add</button>
        <button >fetch</button>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px', borderTop: '1px solid black' }}>
        <ul style={{ textAlign: 'center', }}>
          {users?.map(user => (
            <li key={user.id}>
              {user.name}
              <button style={{ marginLeft: '15px' }}>delete</button>
            </li>
          ))}

        </ul>
      </div>
    </div>
  );
}

export default App;
```
We have imported our **useStore** hook from our store, used it to store the users state in a **users** variable, which we then  render out as list of users 

![accessing store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmvuemondgv3rqicc6km.jpg)

## Updating our State
To be able to update our state, we will create 2 new properties in our store, **addUser** and **deleteUser** which we will use to update our state immutably. The _store.js_ file now looks like this

```
import create from 'zustand';

let store = (set) => ({
    users: [{ id: 1, name: 'Teyim Asobo' }, { id: 2, name: 'Fru Brian' }],
    addUser: (user) => set((state) => ({ users: [...state.users, user] })),
    deleteUser: (userId) => set((state) => ({ users: state.users.filter(user => user.id !== userId) }))
})

export const useStore = create(store);
```
Now, back in our App.js component, we store the **addUser** and **deleteUser** store properties in variables.

```
import shallow from 'zustand/shallow'
import { useStore } from './store'

const { addUser, deleteUser, users} = useStore(
    (state) => ({ users: state.users, addUser: state.addUser, deleteUser: state.deleteUser}),
    shallow
  )
```
Here, we construct a single object with multiple state-picks for a cleaner and better looking code, rather than using atomic state picks. **Shallow**  is a function we import from Zustand. by default, when using atomic state picks, Zustand detects changes with strict-equality (old === new). when constructing a single object with multiple picks, this is not efficient because the object will always be recreated and hence (oldObject !== newObject) therefore will not trigger a UI update. The **Shallow** function tells Zustand to compare state values by moving into the object itself and comparing its keys, if any one is different, then it triggers an update. 
our _App.js_ file now looks like this

```
function App() {
  const { addUser, deleteUser, users } = useStore(
    (state) => ({ users: state.users, addUser: state.addUser, deleteUser: state.deleteUser}),
    shallow
  )
  const inputRef = useRef();

  const addUserHandler = () => {
    addUser({ id: users.length + 1, name: inputRef.current.value })
    inputRef.current.value = ''

  }

  return (
    <div>
      <div style={{ justifyContent: 'center', display: 'flex', padding: '15px' }}>
        <input type={'text'} placeholder='enter user name' style={{ padding: '10px,15px' }} ref={inputRef} />
        <button onClick={addUserHandler}>Add</button>
        <button>fetch</button>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px', borderTop: '1px solid black' }}>
        <ul style={{ textAlign: 'center', }}>
          {users?.map(user => (
            <li key={user.id}>
              {user.name}
              <button onClick={() => { deleteUser(user.id) }} style={{ marginLeft: '15px' }}>delete</button>
            </li>
          ))}

        </ul>
      </div>
    </div>
  );
}

export default App;

```
**addUser** and **deleteUser** can be used as normal functions to add and remove users from state.

## Handling Async actions
Zustand makes storing async data in state very easy. To demonstrate this, we are going to use [mockapi.io](mockapi.io) to create some dummy data which we can access via an API which [mockapi.io](mockapi.io) provides.

```
let store = (set) => ({
    users: [{ id: 1, name: 'Teyim Asobo' }, { id: 2, name: 'Fru Brian' }],
    addUser: (user) => set((state) => ({ users: [...state.users, user] })),
    deleteUser: (userId) => set((state) => ({ users: state.users.filter(user => user.id !== userId) })),
    fetchUser: async (endpoint) => {
        const reponse = await fetch(endpoint)
        const userData = await reponse.json()
        set({ users: userData })
    }
})
```
We recieve an api endpoint via the fetchUser property, fetch the data from the endpoint, convert this data to JSON and then use the **set** function to set this data to our state.
We can then call and use **fetchUser** in our _App.js_ component.

```
function App() {
  const { addUser, deleteUser, users, fetchUser } = useStore(
    (state) => ({ users: state.users, addUser: state.addUser, deleteUser: state.deleteUser, fetchUser: state.fetchUser }),
    shallow
  )
  const inputRef = useRef();

  const addUserHandler = () => {
    addUser({ id: users.length + 1, name: inputRef.current.value })
    inputRef.current.value = ''

  }

  return (
    <div>
      <div style={{ justifyContent: 'center', display: 'flex', padding: '15px' }}>
        <input type={'text'} placeholder='enter user name' style={{ padding: '10px,15px' }} ref={inputRef} />
        <button onClick={addUserHandler}>Add</button>
        <button onClick={() => fetchUser('https://62fa5e3affd7197707eb05e4.mockapi.io/users')}>fetch</button>
      </div>
      <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20px', borderTop: '1px solid black' }}>
        <ul style={{ textAlign: 'center', }}>
          {users?.map(user => (
            <li key={user.id}>
              {user.name}
              <button onClick={() => { deleteUser(user.id) }} style={{ marginLeft: '15px' }}>delete</button>
            </li>
          ))}

        </ul>
      </div>
    </div>
  );
}

export default App;
```
The API endpoint gives us a list of 50 users, which we then store in our state. The output looks like this

![list of users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p8v0a4vu2vw8bht3lyj9.jpg)

## Persisting data and using Devtools
Zustand provides easy to use middlewares, which we can use to persist data, and to track state via redux devtools. by default, Zustand persist data using **localstorage** hence state is not lost after page refresh. A common use case may be when dealing with Auth state, or a simple cart functionality in your E-commerce application where the user still sees a list of cart items even after re-visting the site. To persist our app state, we import the persist middleware from Zustand

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

let store = (set) => ({
    users: [],
    addUser: (user) => set((state) => ({ users: [...state.users, user] })),
    deleteUser: (userId) => set((state) => ({ users: state.users.filter(user => user.id !== userId) })),
    fetchUser: async (endpoint) => {
        const reponse = await fetch(endpoint)
        const userData = await reponse.json()
        set({ users: userData })
    }
})

store = persist(store, { name: 'user' })
export const useStore = create(store);
```
When we now click on the fetch button to fetch a list of users from our API endpoint which is then stored in state, our state is persisted in **local storage** with key _users_.

![local storage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2l7egvxuh0kyw2b78sug.jpg)

## Using Devtools
Zustand provides a middleware called _devtools_, which we can import and use to track our state changes using the [Redux Dev tools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) browser extension. you might be familiar with this if you have worked with Redux before. 

```
store = persist(store, { name: 'users' })
store = devtools(store);
export const useStore = create(store);
```
Above, we have persisted our store in local storage and passed our store to our Devtools middleware. We can now monitor and track our state in the browser.

![Redux devtools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/385ti61ttzxfq78lshzm.jpg)

## Conclusion
Zustand is a state management library that helps developers to manage application state, with its strong holds being it having a small size and easy to setup. Relative to other libraries like Redux, Zustand helps developers manage complex application state using hooks. After reading this guide, you should be able to :

- Setup Zustand in your React application
- Create a store
- Access and modify state in store
- Store data in state via Async request
- Persist state in local storage and use Redux dev tools to track and monitor state.