Harness State Management Using Zustand
The state management solution for React
State management is the process of maintaining and sharing the state of the application’s data across multiple related data flows and components without prop drilling to child components.
What is Zustand?
To quote from the official docs:
“A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks and isn’t boilerplatey or opinionated.”
And from personal experience, I can stand by it and would go a step beyond to back it as the simplest state management library I’ve used for react or even vanilla JavaScript.
Why Zustand?
- Simple and un-opinionated when compared to its competitors
- Uses hooks as the primary means of consuming state
- No need for any context provider or any wrapper/higher-order components
- Little to no boilerplate when compared to its competitors
Implementation
Let's get started, shall we?
Step 1:
Install zustand inside your react app by running:
npm i zustand
or yarn add zustand
Step 2:
Create a folder named store
. Your state management logic should go in this folder. I’ll name mine store/storeOne.js
to start.
Step 3:
Create a store by importing create
from zustand:
import create from "zustand";
and then use create
to create your first store:
const useStoreOne = create();
Step 4:
To add values and actions to the store, create
takes in a callback function that allows you to set and get values. The callback function takes in two arguments set
and get
. set
is used to set values and get
as the name suggests. It is used to get values within the store.
We’re just going to use set
for now, we’ll get back to get
later.
The callback function should return an object with the store's values and functions. So to add a value, we’d do something like this:
const useStoreOne = create((set) => ({
value: 0,
}));
And adding an action is similar as well, assign an anonymous function to create an action and then use set
to set the updated value. set
takes in a callback function to which the current state of the store is passed, similar to how useState
works.
const useStoreOne = create((set) => ({
value: 0,
increaseValue: () => set((state) => ({ value: state.value + 1 })),
decreaseValue: () => set((state) => ({ value: state.value - 1 })),
}));
Step 5:
You are just an export away from accessing your store. Let’s add the export statement.
export default useStoreOne;
And now, you’re done creating your store! At the end of the implementation, your store should look like this:
import create from "zustand";
const useStoreOne = create((set) => ({
value: 0,
increaseValue: () => set((state) => ({ value: state.value + 1 })),
decreaseValue: () => set((state) => ({ value: state.value - 1 })),
}));
export default useStoreOne;
Accessing The Store
As I mentioned earlier, you don’t need a context provider to consume or access the store. You have to import the store wherever you want to use the value or action from the store. And it’s really simple to access the value and action from the store too. Here’s a simple app to increase and decrease value.
import useStoreOne from "./store/storeOne";
function App() {
const { value, increaseValue, decreaseValue } = useStoreOne();
return (
<>
<button onClick={increaseValue}>+</button>
<h1>{value}</h1>
<button onClick={decreaseValue}>-</button>
</>
);
}
export default App;
As you can see, we’re destructuring all our values and actions from the store. There are multiple ways to do the same thing. Here are some of them:
// 1
const { value, increaseValue, decreaseValue } = useStoreOne();
// 2
const { value, increaseValue, decreaseValue } = useStoreOne((state) => ({
value: state.value,
increaseValue: state.increaseValue,
decreaseValue: state.decreaseValue,
}));
// 3
const value = useStoreOne((state) => state.value);
const increaseValue = useStoreOne((state) => state.increaseValue);
const decreaseValue = useStoreOne((state) => state.decreaseValue);
// 4
const [value, increaseValue, decreaseValue] = useStoreOne((state) => [
state.value,
state.increaseValue,
state.decreaseValue,
]);
Although I’m using the first method in the app above, you should avoid using it. The reason? Here’s a quote from a blog I came across when I was researching:
// ❌ we could do this if useBearStore was exported
const { bears } = useBearStore()
While the result might be the same, you’ll get the number of bears. The code above will subscribe you to the entire store, which means that your component will be informed about any state update, and therefore rerendered, even if bears did not change, e.g., because someone ate a fish.
While selectors are optional in Zustand, I think they should always be used. Even if we have a store with just a single state value, I’d write a custom hook solely to be able to add more state in the future.
In short, you end up subscribing to all the values and actions of the store, which might cause many unexpected and unwanted rerenders.
Accessing Values/Actions Within a Store
As promised earlier, we’ll now discuss about get
. get
should be ideally used to access a value or an action to do something with the value or action and not to update states. For example, you could do the following:
const useStoreOne = create((set, get) => ({
value: 0,
increaseValue: () => set({ value: get().value + 1 }),
}));
But get().value
might not get you the most up-to-date state of the value
. Whereas something like:
const useStoreOne = create((set, get) => ({
value: 0,
increaseValue: () => set((state) => ({ value: state.value + 1 })),
decreaseValue: () => set((state) => ({ value: state.value - 1 })),
storeActions: () => [get().increaseValue, get().decreaseValue],
}));
would be ideal as you are not changing the states directly with get
.
Accessing Values/Actions from Another Store
In bigger projects, you’d run into use cases where you’d be required to update the states of another store through a different store or even access certain actions from a different store. For such instances, you won’t be able to access them through the more reactive way we’ve discussed already, but you’ve to use more of a vanilla JavaScript way to access them. For example:
import create from "zustand";
import { storeOne } from "./storeOne";
const useStoreTwo = create(() => ({
setStoreOneValue: (newValue) => storeOne.setState({ value: newValue }),
getStoreOneActions: () => storeOne.getState().storeActions(),
}));
export default useStoreTwo;
In the above snippet, we use setState
to update the state of storeOne
and getState
to get the state/action of storeOne
.
Conclusion
This article aims to help anyone looking to switch to Zustand or to show that there are simpler ways to achieve state management without using Redux or any other conventional state management solutions.
If you’d like to delve deeper into Zustand, you can look into the following: