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.