DEV Community

Cover image for Using Zustand to Manage State in React App
Teyim Asobo
Teyim Asobo

Posted on • Updated on

Using Zustand to Manage State in React App

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

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
Enter fullscreen mode Exit fullscreen mode

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

npm install zustand
yarn add zustand
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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 to create some dummy data which we can access via an API which 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 })
    }
})
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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);
Enter fullscreen mode Exit fullscreen mode

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

Using Devtools

Zustand provides a middleware called devtools, which we can import and use to track our state changes using the Redux Dev tools 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);
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (3)

Collapse
 
naucode profile image
Al - Naucode

I really like that library, good work explaining how it works, hopefully with this article more people will onboard using this library!

Collapse
 
teyim profile image
Teyim Asobo

Yeah..it's an amazing library.way less complicated than things like redux or redux toolkit.. hopefully many more people get to know and start playing around with it.

Collapse
 
activenode profile image
David Lorenz

Awesome article. Also feel free to check out unglitch: dev.to/activenode/unglitch-ultra-s...