Getting started with Zustand state management for React

Zustand is a simple, fast "bearbones" (yeah bear, not bare!) state management library for React. It can (and should) be used in place of more complicated libraries like Redux or MobX. Built on top of the Context API, Zustand provides a simple, intuitive and scalable API for managing global state in your React components. Today I'm going to show you how to get started with Zustand using code from my latest project, which is an application that, in part, allows you to bookmark tv shows and movies you are interesting in watching. The bookmark icon appears at the top right corner of every item card. An item card has a movie/series poster and some basic information. The same item might appear in many places, including the search page, the trending component, the user's watchlist, the slide show, etc. When an item is bookmarked, the bookmark SVG shows a checkmark otherwise it is an empty bookmark. So the state of the item, bookmarked or not, must be shared across many components and clicking the button must result in instantaneous changes of the SVG across all components. In other words, global state management is now a necessity. As with any library, we start by installing it. npm i zustand (I use npm but you can use your preferred package manager, of course) Our next step is to set up a store. For any of you who have worked with other state managers, you may be familiar with the concept of slices. Consider your global state a pie, and there are different slices that revolve around different state requirements. You may have a slice for user information, a slice for authentication, a slice for bookmarking functionality, etc. Since today is a basic introduction, I don't want to complicate this with slicing. It's not required anyway, just good practice. I have a file called store.ts that I will use for creating my store. Yes, I am using TypeScript (TS), cue the groans, and then let's get on with it! You may know that global state is erased from memory if the user reloads the page, so I am going to show you persistence right at the same time as I show you the state management because it is SO EASY and honestly, you should have it anyway. Do you really want your user logged out just because they hit reload? They don't want that either. I'm going to go with local storage for this, but Zustand supports other types, including async storage. I am also going to push things just a bit more by showing you partial storage, because I really think you should know this right up front as well. You might have a large store for your app, but probably you won't want it ALL to persist, especially knowing local storage is quite limited at 5-10 MB depending on the browser. You can set up for partial persistence even if you DO end up saving everything and it won't cause any problems. At the top of our store.ts the imports will be: import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; Create is going to help us create the store, persist will help us persist the data, and createJSONStorage is how you use local storage. I will make the bookmarks an array and I will need functions to add a bookmark and remove a bookmark (this code is more performant if you use an object instead of an array, but for purposes of this lesson, we will do it this way). interface BookmarkStore { bookmarks: { id: string; media_type: string }[]; addBookmark: (_id: string, _type: string) => void; removeBookmark: (_id: string, _type: string) => void; } This is our TS declaration as to what bookmarks and add/remove functions look like. A bookmark holds the item's id and media type, movie or tv, and it is an array. You can skip this if you aren't using TS. To set up the store: export const useBookmarkStore = create()( persist( (set) => ({ bookmarks: [], The actual bookmarks array is declared here as an empty array. Set will update state with new data. There is also "get" that you can add right after set, (set,get) if you need to look at something in state before you set, or you need to write a getter function. Now for the add and remove functions, added directly under the bookmarks empty array: addBookmark: (id, media_type) => set((state) => ({ bookmarks: [ ...state.bookmarks, { id, media_type, dateAdded: dayjs().unix() }, ], })), removeBookmark: (id, media_type) => set((state) => ({ bookmarks: state.bookmarks.filter( (b) => !(b.id === id && b.media_type === media_type), ), })), In keeping with our functional style of programming, we create a new array with the added or removed bookmark each time we add or remove one. Note I have added a third key/value pair to the bookmarks array, dateAdded, but you can disregard that. If you do want this same functionality don't forget to install and import Day.js. Next I will add the partial persistence

Apr 6, 2025 - 05:49
 0
Getting started with Zustand state management for React

Zustand is a simple, fast "bearbones" (yeah bear, not bare!) state management library for React. It can (and should) be used in place of more complicated libraries like Redux or MobX. Built on top of the Context API, Zustand provides a simple, intuitive and scalable API for managing global state in your React components.

Today I'm going to show you how to get started with Zustand using code from my latest project, which is an application that, in part, allows you to bookmark tv shows and movies you are interesting in watching. The bookmark icon appears at the top right corner of every item card. An item card has a movie/series poster and some basic information. The same item might appear in many places, including the search page, the trending component, the user's watchlist, the slide show, etc. When an item is bookmarked, the bookmark SVG shows a checkmark otherwise it is an empty bookmark. So the state of the item, bookmarked or not, must be shared across many components and clicking the button must result in instantaneous changes of the SVG across all components. In other words, global state management is now a necessity.

As with any library, we start by installing it.
npm i zustand
(I use npm but you can use your preferred package manager, of course)

Our next step is to set up a store. For any of you who have worked with other state managers, you may be familiar with the concept of slices. Consider your global state a pie, and there are different slices that revolve around different state requirements. You may have a slice for user information, a slice for authentication, a slice for bookmarking functionality, etc. Since today is a basic introduction, I don't want to complicate this with slicing. It's not required anyway, just good practice.

I have a file called store.ts that I will use for creating my store. Yes, I am using TypeScript (TS), cue the groans, and then let's get on with it!

You may know that global state is erased from memory if the user reloads the page, so I am going to show you persistence right at the same time as I show you the state management because it is SO EASY and honestly, you should have it anyway. Do you really want your user logged out just because they hit reload? They don't want that either. I'm going to go with local storage for this, but Zustand supports other types, including async storage. I am also going to push things just a bit more by showing you partial storage, because I really think you should know this right up front as well. You might have a large store for your app, but probably you won't want it ALL to persist, especially knowing local storage is quite limited at 5-10 MB depending on the browser. You can set up for partial persistence even if you DO end up saving everything and it won't cause any problems.

At the top of our store.ts the imports will be:

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

Create is going to help us create the store, persist will help us persist the data, and createJSONStorage is how you use local storage. I will make the bookmarks an array and I will need functions to add a bookmark and remove a bookmark (this code is more performant if you use an object instead of an array, but for purposes of this lesson, we will do it this way).

interface BookmarkStore {
  bookmarks: { id: string; media_type: string }[];
  addBookmark: (_id: string, _type: string) => void;
  removeBookmark: (_id: string, _type: string) => void;
}

This is our TS declaration as to what bookmarks and add/remove functions look like. A bookmark holds the item's id and media type, movie or tv, and it is an array. You can skip this if you aren't using TS. To set up the store:

export const useBookmarkStore = create()(
  persist(
    (set) => ({
       bookmarks: [],

The actual bookmarks array is declared here as an empty array. Set will update state with new data. There is also "get" that you can add right after set, (set,get) if you need to look at something in state before you set, or you need to write a getter function. Now for the add and remove functions, added directly under the bookmarks empty array:

      addBookmark: (id, media_type) =>
        set((state) => ({
          bookmarks: [
            ...state.bookmarks,
            { id, media_type, dateAdded: dayjs().unix() },
          ],
        })),

      removeBookmark: (id, media_type) =>
        set((state) => ({
          bookmarks: state.bookmarks.filter(
            (b) => !(b.id === id && b.media_type === media_type),
          ),
        })),

In keeping with our functional style of programming, we create a new array with the added or removed bookmark each time we add or remove one. Note I have added a third key/value pair to the bookmarks array, dateAdded, but you can disregard that. If you do want this same functionality don't forget to install and import Day.js. Next I will add the partial persistence code:

  }),
{
      name: 'bookmarks-storage',
      storage: createJSONStorage(() => localStorage),
      partialize: (state) => ({
        bookmarks: state.bookmarks,

      }),
    },
  ),
);

This code sets the name of your choosing for the storage. It tells Zustand to use local storage to persist data, and it says which of the above data to store. Now if we add any more state to our store, we can decide to include it in the partialize function or not - we have control. As I said, I just prefer setting things up this way from the outset rather than refactoring later when I have state I don't want persisted.

That's it for the store! Really simple, yet with some really nice additions, persist and partialize.

Let's just take a quick look at how to use this, and you'll be all set to give it a try for yourself.

In a component that requires knowledge of the bookmarks (I'll use the watchlist component), we can gain access to the bookmarks array with a selector and thus gain reactivity to the state changes so the bookmark's SVG is always correctly toggled to the correct view (checkmark if bookmarked else empty).

import { useBookmarkStore } from '../state/store';

const Watchlist = () => {
  const bookmarks = useBookmarkStore((state) => state.bookmarks);

In my item card, I simply send it a prop isBookmarked={true}.

It can be a bit more complicated if you are in a component that is not entirely comprised of bookmarked items. You will still import the store and create the selector for the store's state, but to figure out if any one particular item is bookmarked will require some extra work:

         allItems.map((item: IItem) => (
               a.id === item.id && a.type === itemType,
                )}
              />
           )

Now some of you will immediately see why an object is a better choice for the bookmarks state than an array! An object can be accessed in O(1) time by simply using it's key. Here we have to iterate through the array in O(n) time to see if some id is matched in the array. This would be a bottleneck in our performance if the bookmarks array got large.

A final bit of code to illustrate the add and remove functions:

 const { addBookmark, removeBookmark } = useBookmarkStore();

 const handleBookmarkToggle = () => {
    if (isBookmarked) {
      removeBookmark(id, media_type);
    } else {
      addBookmark(id, media_type);
    }
  };

All we do here is simply destructure the add and remove functions from the store and then use them in an onClick function! (not showing the import statement, but it's the same as you saw before, don't forget it).

How easy is Zustand? It's my favorite! And did you notice, I'm sure you did, we don't have to stringify and parse the local storage data ourselves? So helpful!

I hope you enjoyed this getting started tutorial and will check out Zustand for yourselves!