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.
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>;
}
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
})
}
})
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;
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
})
))
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>
)
}
I hope this was helpful, and if you have any questions or suggestions please reach out.