Global Zustand Store in React 🌎🐻

Matt Lewandowski - Jan 27 - - Dev Community

State Management

Zustand is an amazing state management tool. It's often used to smaller pieces of state, spread throughout an application. However, it also works well for monolithic application states or several large states. Meaning it can easily replace your global redux state.

Today I want to share a pattern with you, that I have been using for very large and complex states in my react applications.

More than just a bear state 🐻

In order to keep our complex state simple, it's important to break things up into logical chunks. In this example we have a global bear state, which consists of a lot of smaller stores that handle specific bears needs like clothes and food.

First lets take a look at the structure of our state.
Image description

For this demo, we are going to keep all of our state inside of the state directory. However, in a real application, I might take a more redux-like approach, and keep all of the sub-stores closer to the feature directory. In this example, the clothes and food stores would be the sub-stores.

SubStores

Each sub-store should have a type file, and a file for the store. This is to prevent circular dependencies since we will need to import this file in our main store types file.

Let's take a look at the food store types.ts



export type BearFoodState = {
    fruits: [];
    vegetables: [];
    setFruits: (fruits: []) => void;
    setVegetables: (vegetables: []) => void;
    fetchFood: () => Promise<void>;
}


Enter fullscreen mode Exit fullscreen mode

Nothing too crazy going on here. Just a few things that would be specific to this store.

Now let's take a look at the bear-food-store.tsx file.



import {SubStore} from "../types";
import {BearFoodState} from "./types";

export const getBearFoodStore:SubStore<BearFoodState> = (set, get) => ({
    fruits: [],
    vegetables: [],
    setFruits: (fruits) => set((s) => {
        s.food.fruits = fruits;
    }),
    setVegetables: (vegetables) => set((s) => {
        s.food.vegetables = vegetables;
    }),
    fetchFood: async () => {
        const response = await fetch('https://fakeapi/getFood');
        const food = await response.json();

        set((s) => {
            s.food.fruits = food.fruits
            s.food.vegetables = food.vegetables
        })
    }
})


Enter fullscreen mode Exit fullscreen mode

Here we are implementing the sub-store. You'll notice we aren't using create from zustand here. This is because we don't want to create another store. This store is just an extension of our main store. So we have a custom type that we have created, which lets us define that contents of this store, while still using set and get.

You might also notice we are setting variables directly. I highly recommend using immer with zustand, so you are not spreading objects everywhere.

Now we just do this same thing for our other sub stores.

The Main Store

Let's start off with the main store types.ts file again.



import {BearFoodState} from "./bear-food-store/types";
import {BearClothesState} from "./bear-clothes-store/types";

import type {WritableDraft} from "immer/src/types/types-external";

export type BearsState = {
    clothes: BearClothesState;
    food: BearFoodState;
}

export type SubStore<T> = (
    set: (
        nextStateOrUpdater:
            | BearsState
            | Partial<BearsState>
            | ((state: WritableDraft<BearsState>) => void),
        shouldReplace?: boolean,
    ) => void,
    get: () => BearsState,
) => T;


Enter fullscreen mode Exit fullscreen mode

Our main bear state is simple. It's just a wrapper around the substores that we have defined. This means our main bear state will need to import the substates, for its definition.

Now that we have a definition of our main store, we can create our SubStore type. This type is used to create sub-store functions. When used like the example, it will create a typesafe sub-store.

Now we can create our store.



import {create} from "zustand";
import {immer} from "zustand/middleware/immer";

import {BearsState} from "./types";

import {getBearClothesStore} from "./bear-clothes-store/bear-clothes-store";
import {getBearFoodStore} from "./bear-food-store/bear-food-store";

export const useBearStore = create<BearsState>()(
    immer((set, get) => ({
        clothes: getBearClothesStore(set, get),
        food: getBearFoodStore(set, get),
        ...more sub-stores that you create
    })
))


Enter fullscreen mode Exit fullscreen mode

Here we import zustand and immer to create the store. We use the main bears state as the type, and import the substore functions to create the stores. It's as simple as that.

Using The Store

Nothing has really changed when it comes to using the store values. You can just use your main bear state, and drill down into each sub-store.



export const App = () => {
    const shirts = useBearStore((s) => s.clothes.shirts);
    const fruit = useBearStore((s) => s.food.fruits);

    return (
        <div>
            <h1>Bear Shirts</h1>
            <ul>
                {shirts.map((shirt) => (
                    <li key={shirt}>{shirt}</li>
                ))}
            </ul>
            <h1>Bear Fruits</h1>
            <ul>
                {fruit.map((fruit) => (
                    <li key={fruit}>{fruit}</li>
                ))}
            </ul>
        </div>
    )
}


Enter fullscreen mode Exit fullscreen mode

I hope this was helpful, and if you have any questions or suggestions please reach out.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .