Simple & Elegant State-Management with Zustand
Tags: webdev, javascript, frontend, react
Published: 2024-03-06 17:39:24

Have you ever wanted to learn state-management but it felt so overwhelming to get started with Redux? Or maybe you're already using Redux but don't want to take the burden anymore? If so, you might want to take a shot at [Zustand](https://github.com/pmndrs/zustand)! Its an amazing state management library that's easy to get started and maintain in a long run. In this post, we'll dive into creating & sharing stores with this tiny Redux alternative.

## Why Zustand?

- Small & Performant
- Beginner friendly
- Rapidly adopted by Frontend devs

In this tutorial, we'll see usage of Zustand directly on React (via Vite). I'll likely add another chapter for using it with Next.js too.

## Setup

Fire up your favorite code editor and let's get started! We'll first need to create a fresh project and install the library. I'll be using Node.js as runtime and Bun as package manager throughout the tutorial. You can use whatever suits to you!

```sh
$ bun create vite
✔ Project name: … zustand-episode-1
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC

Scaffolding project in zustand-episode-1...

Done. Now run:

  cd zustand-episode-1
  bun install
  bun run dev
```

After you've installed the dependencies, install the Zustand as the following:

```sh
bun add zustand
```

Cool! I've also removed everything inside `App.css`, `index.css` and `App` component:

```tsx
function App() {
  return <></>;
}

export default App;
```

## Creating a Store

What we need first is a store. Store is where we put our states and methods. Its possible to keep primitives, objects and functions in them. Import `create` from Zustand and create your own:

```tsx
import { create } from "zustand";

const useCountStore = create(() => ({ count: 0 }));

function App() {
  const count = useCountStore((state) => state.count);

  return <>{count}</>;
}

export default App;
```

Notice that `create` takes a callback that returns an object (It is `() => ({})` **not** `() => {}`). The object we return is our store. `create` function also gives back a hook that we can use to access our store. Inside the `App` component, we have used the hook to retrieve the `count`.

That's cool but surely we'd like to mutate the `count`.

## Store Actions

The callback we've passed to `create` can optionally take `set` argument which is a function that we can use to mutate our state. Modify the `useCountStore` hook as the following:

```tsx
const useCountStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
```

**Let's run it down;**

1. `increment` is a function that calls `set`,
2. `set` takes `state` as an argument which is the current state (similar to set function we get from [useState](https://react.dev/reference/react/useState#setstate)) and returns a new object where the it takes `state.count` and increments by 1,
3. same applies to decrement but instead it decrements the `count` by 1.

Now that we've created our **actions**, let's use them in our component to mutate the `count`. We can access to our actions like this:

```tsx
function App() {
  const count = useCountStore((state) => state.count);
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);

  ...
```

Or, we can take advantage of destructive assignment:

```tsx
function App() {
  const { count, increment, decrement } = useCountStore();

  ...
```

Much cleaner! I've also created a barebones interface to play around:

```tsx
function App() {
  const { count, increment, decrement } = useCountStore();

  return (
    <>
      {/* Count value */}
      <span>{count}</span>
      <br />
      {/* Button that increments the count value */}
      <button type="button" onClick={increment}>
        increase
      </button>
      {/* Button that decrements the count value */}
      <button type="button" onClick={decrement}>
        decrease
      </button>
    </>
  );
}
```

You should be able to see the current count value and increment/decrement it.

## Accesing the Store from Another Component

If we're persisting a store outside of our component, we most likely want to access it from some other component. This is also one of the great things about using a state manager, it avoids **prop drilling**.

Right now our store lives inside `App.tsx`. Let's start by moving it to somewhere else. How about a `stores` folder inside `src`? That sounds like a good idea. Or, we could approach it just like an hook and move it to `hooks` folder. I'll go with the former but like I said earlier, whatever suits. My folder structure looks like this now:

```
node_modules/
public/
src/
  stores/
    count-store.ts
  App.tsx
  main.tsx
  ...
```

In `stores/count-store.ts`, I've took the code for our store and pasted it there. Then I've added `export` keyword right before `useCountStore` declaration.

`stores/count-store.ts`
```ts
import { create } from "zustand";

export const useCountStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
```

While at it, we can also add some typing to make our code more understandable:

`stores/count-store.ts`
```ts
import { create } from "zustand";

// States are typed here
interface CountState {
  count: number;
}

// Actions (or functions) are typed here
interface CountActions {
  increment: () => void;
  decrement: () => void;
}

// We create a unified type from them
type CountStore = CountState & CountActions;

// Indicate the type here
export const useCountStore = create<CountStore>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));
```

This is self-explanatory. If you didn't know, `&` in TypeScript merges the types.

Let's also create a new component in `src` named `Counter.tsx` and fill it in with the following:

`src/Counter.tsx`
```tsx
import { useCountStore } from "./stores/count-store";

export default function Counter() {
  const count = useCountStore((state) => state.count);

  return <span>{count}</span>;
}
```

Sole purpose of this component is to show `count` value, I'm aware that we're already doing it in `App` but the idea here is to show that we can access the same store from some other component.

In `App.tsx`, import our `Counter` component and render it alongside our buttons:

```tsx
import Counter from "./Counter";
import { useCountStore } from "./stores/count-store";

function App() {
  const { increment, decrement } = useCountStore();

  return (
    <>
      {/* Count value */}
      <Counter />
      <br />
      {/* Button that increments the count value */}
      <button type="button" onClick={increment}>
        increase
      </button>
      {/* Button that decrements the count value */}
      <button type="button" onClick={decrement}>
        decrease
      </button>
    </>
  );
}

export default App;
```

Notice that we're only mutating the `count` in `App.tsx`. The rendering of `count` is moved to `Counter.tsx` and it still works perfectly! If our tree would be deeply nested, it still won't be a problem for us to access our store and read/mutate the state.

That's it for part 1. I hope you got the hang of Zustand and liked it! In the follow-up, we'll be trying to integrate React Context API to this and I'll also show you how you can use Zustand in your Next.js apps. Take care!