Better Programming

Advice for programmers.

Follow publication

Harness State Management Using Zustand

Abhishek KM
Better Programming
Published in
5 min readJan 10, 2023
Photo by Dennis Siqueira on Unsplash

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?

  1. Simple and un-opinionated when compared to its competitors
  2. Uses hooks as the primary means of consuming state
  3. No need for any context provider or any wrapper/higher-order components
  4. 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:

Write a response