Skip to content

Using Zustand in Your Next React Project

Introduction

This article, we will focus more on the practical use of Zustand in a real world project. You can check this article Zustand for State Management if you need an introductory read on the topic. We will be focusing on making API calls right from our Zustand store, and also persisting state to either session or local storage.

The project we are going to build is a GitHub user search page where we can experiment with GitHub API. We will be able to search for users, and get more information about a user. We will also see how we can persist the store state.

Project Set Up

To get started, we need to create a new React app. We can do this by running the following command in our terminal:

npx create-react-app zustand-demo --template typescript

Project Dependencies

After the project is created, we need to install our project dependencies. We can do this by running the following command in our terminal:

yarn add zustand @chakra-ui/react @emotion/react @emotion/styled react-icons react-router-dom axios framer-motion pluralize query-string react-helmet-async react-hook-form react-paginate 

Project Structure

After installing our project dependencies, we need to create our project structure. We can do this by creating the following folders in our project:

    src
    ├── index.tsx
    ├── assets
    ├── container
    ├── components
    ├── pages
    ├── routes
    ├── services
    ├── store
    └── theme

ENV Variables

After creating our project structure, we need to create our env variables. We can do this by creating a .env file in our project root, and adding the following variables:

REACT_APP_GITHUB_API_URL=https://api.github.com

Services

After creating our env variables, we need to create our services. We can do this by creating a githubService.ts file in our services folder, and adding the following code:

    import axios from 'axios';
    import qs from 'query-string';

    const github = axios.create({
        baseURL: process.env.REACT_APP_GITHUB_API_URL,
    });

    interface IGet {
        url: string;
        query?: Record<string, any>;
    }

    const get = async <T>({ url, query = {} }: IGet): Promise<T> => {
        const queryString = `?${qs.stringify(query)}`;
        const response = await github.get(`${url + queryString}`);
        return response.data;
    };

    const methods = { get };

    export default methods;

Store Setup

Next, we will set up our Github store. We will be implementing the Zustand persist method to persist our state to either session or local storage.

import create from 'zustand';
import { stringify } from 'query-string';
import { persist } from 'zustand/middleware';

import methods from 'services/github';
import { IUserDetails, ISearchResonse, IGithubStore } from 'stores/types';

export const githubStore = create(
  persist<IGithubStore>(
    (set, get) => ({
      isLoading: false,
      cached_users_details: [], // to cache users details
      query: { page: 1, per_page: 20 },
      search: async (query) => {
        try {
          set(() => ({ isLoading: true }));
          window.history.pushState('', '', `?${stringify(query)}`);
          const data = await methods.get<ISearchResonse>({
            url: '/search/users',
            query,
          });
          set(() => ({ data, query, isLoading: false }));
        } catch (err: any) {
          const error =
            err?.message || err?.data?.message || 'Unexpected network error.';
          set(() => ({ isLoading: false, error }));
        }
      },
      getUser: async (username) => {
        try {
          set(() => ({ isLoading: true }));
          // check if user is already cached
          const userDetails = get().cached_users_details.find(
            (u) => u.login === username
          );
          if (userDetails) {
            set(() => ({ userDetails, isLoading: false }));
          } else {
            const userInfo = await methods.get<IUserDetails>({
              url: `/users/${username}`,
            });

            set((state) => ({
              cached_users_details: [...state.cached_users_details, userInfo],
              userDetails: userInfo,
              isLoading: false,
            }));
          }
        } catch (err: any) {
          const error =
            err?.message || err?.data?.message || 'Unexpected network error.';
          set(() => ({ isLoading: false, error }));
        }
      },
    }),
    {
      name: 'search-storage',
      getStorage: () => sessionStorage,
    }
  )
);

We were able to make an asynchronous call to the GitHub api, and set the response to our store right from our store nicely without having to use extra middleware. We also cached our users details so that we don't have to make another API call to get the same user details again. We also persisted our state to session storage. We can also persist our state to local storage by changing the getStorage method to localStorage.

Clear/Reset Store

Right now, there is no out of the box method from Zustand. But we can do that by adding a clear/reset method to our store, resetting our state back to the initial state and calling the sessionStorage or localStorage clear() method.

    const initialState = {
        data: undefined,
        userDetails: undefined,
        cached_users_details: [],
        query: { page: 1, per_page: 20 },
        isLoading: false,
        error: undefined,
    };

    export const githubStore = create(
        persist<IGithubStore>(
            (set, get) => ({
                ...initialState,
                ...
                clear: () => {
                    set(() => (initialState));
                    sessionStorage.clear(); // or localStorage.clear();
                },
         })
        )
    );

A link to the project repo can be found here, and a link to the live demo can be found here.

If you have any questions or run into any trouble, feel free to reach out on Twitter or Github.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.