Zustand Decoded: The Art of Simple and Scalable State Management in React

Zustand Decoded: The Art of Simple and Scalable State Management in React

Introduction

State management is a crutial part of any web application, it helps keeping track of information (state) such as user inputs or app data. There are many different options that are available to use when it comes to this topic. One such library is Zustand.

In today's blog we are going to understand what Zustand is and how it works. This is going to be a series of blogs based on Zustand where we will be starting with simple example and ultimately performing CRUD operations.

Zustand: where bears, beets, and state management meet! ๐Ÿป๐Ÿ  Unleashing the wild side of React with a touch of Dwight's beary charm.

Understanding Zustand

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.

This is the definition on its GitHub repository GitHub - pmndrs/zustand: ๐Ÿป

Just the definition is enough to understand how Zustand works. Here, coding is as important to know as how it actually works behind the scenes.

bearbones - Actually it's called barebones - reduced to the essential elements.

The first key term in the definition is Flux Principles, but what is Flux?

Flux is the application architecture that Facebook uses for building client-side web applications.

The architecture comprises of 4 parts:

  1. Actions

  2. Dispatchers

  3. Store

  4. Views

To understand how Flux architecture works, let us imagine a simple scenario of Supermarket Checkout System.

Imagine being at a supermarket.

  • View:

    • Anything we can see or interact with.

    • e.g.->Each product represents view.

  • Actions:

    • Things you want to do.

    • e.g.->Adding a product to shopping cart is like an Action. For example, putting an apple in the cart is similar to dispatching an "ADD_TO_CART" action.

  • Dispatcher:

    • A traffic controller.

    • e.g.->The cashier is the dispatcher. He/she makes sure that each and every item is processed. It processes one item and then takes another, basically managing the flow of actions.

  • Store:

    • Stores contain the application state and logic.

    • e.g.->When we add items to the cart, the store gets updated.

This was a simple overview, below is the detailed view.

Now that you understand what flux principles is, it would be easy to understand the code, let us take one simple example.

Implementation

I am using Vite, I have initialized the project using yarn create vite

Also while setting up, I have installed Zustand using yarn add zustand

Now when you initialize a project using Vite, the default App.tsx file has a counter example, will replace it by using Zustand later.

Cleaned up the code a little, now there are two button with basic onClick function which will increase and decrease the count by 1.

import { useState } from "react";
import "./App.css";
function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>Increase</button>
        <h1>count is {count}</h1>
        <button onClick={() => setCount((count) => count - 1)}>Decrease</button>
      </div>
    </>
  );
}
export default App;

Now, as Zustand is based on flux architecture, let us create a store. Remember Zustand is unopinionated, meaning that you can have folder and code structure as you like, I personally like to keep it different, so will create a folder named store and have a file named useCountStore.ts.

To use Zustand, we first bring in a special function called create by importing it.

import { create } from "zustand";

We then use this function by providing a set of instructions (a callback function) that defines our state and the actions to change it. The result is a custom hook, like a bundled package, containing our state and functions neatly organized for use in our application.

As I am using typescript, I defined types for my State and Actions.

type State = {
  count: number;
};
type Actions = {
  increment: () => void;
  decrement: () => void;
  reset: () => void;
};

Here is my main code, as you can see, I have defined increment and decrement functions. If you observer carefully there is set parameter provided by zustand within the callback function. It is used to update the state within the store.

const useCountStore = create<State & Actions>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

Now this set function takes an argument, it can either be a function or an object:

  • Function: If we pass a function to set, it receives the current state, and we can use that to calculate the new state. You can see the increment and decrement function, in that function we are using current state to increase or decrease the count by 1.

  • Object: If we pass an object, it will directly replace the current state with the new object. You can see this with reset function.

Let us now export this function.

import { create } from "zustand";

type State = {
  count: number;
};

type Actions = {
  increment: () => void;
  decrement: () => void;
  reset: () => void;
};

const useCountStore = create<State & Actions>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCountStore;

Now we can use this hook anywhere. Let us import this hook in App.tsx file. I have modified the code and now replaced useState with our hook.

import "./App.css";
import useCountStore from "./store/useCountStore";

function App() {
  const { count, increment, decrement, reset } = useCountStore();
  return (
    <>
      <div className="card">
        <button onClick={() => increment()}>Increase</button>
        <button onClick={() => decrement()}>Decrease</button>
        <h1>count is {count}</h1>
        <button onClick={() => reset()}>Reset Count</button>
      </div>
    </>
  );
}

export default App;

Now if you try to increment or decrement, the value of count will be changed globally and you can get the current value of count using the useCountStore hook anywhere in the project.

Conclusion:

As we conclude this introduction to Zustand with a simple example, get ready for the next blog, where we'll delve into more advanced features.

Any questions or tech thoughts? I'm here and excited to connect. Don't miss the upcoming part; make sure to follow for updates.

ย