Revisiting Zustand and React with TS

stark-akshay

Akshay Manoj

Posted on August 5, 2024

Revisiting Zustand and React with TS

This is a simple example which I created to revise the working of Zustand with React and TypeScript with the help of the official Documentation of Zustand
Zustand Official Docs

Installing Zustand using npm

npm install zustand
Enter fullscreen mode Exit fullscreen mode

Defining the types for the values inside the store

We are defining what the type of the state will be and the actions will be so that TypeScript understands while checking it.

type State = {
  firstName: string,
  lastName: string,
  age: number,
}

type Action = {
  updateFirstName: (firstName: State['firstName']) => void;
  updateLastName: (lastName: State['lastName']) => void;
  updateAge: () => void;
}
Enter fullscreen mode Exit fullscreen mode

Creation of a store

A Store is where you initialize the default values for the state and define the actions stating how it should work an manipulate the state (the data set inside the store)

const usePersonStore = create<State & Action>((set) => ({
  firstName: '',
  lastName: '',
  age: 0,
  updateAge: () => set((state) => ({ age: state.age + 1 })),
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),

}))
Enter fullscreen mode Exit fullscreen mode

Usage of this simple store in React

//COMPONENT 1
export default function App() {
    //we use the state of the store to access the variables stored inside it.
  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)
  const age = usePersonStore((state) => state.age);


  return <>
    <label htmlFor="firstName">
      First Name
      <input type="text" name="firstName" id="firstName"
        onChange={(e) => updateFirstName(e.target.value)}
      />
    </label>

    <AgeComp />

    <p>
      Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong>
    </p>
  </>
}

//COMPONENT 2
const AgeComp = () => {
  const updateAge = usePersonStore((state) => state.updateAge)
  return <>
    <Button onClick={updateAge}>+</Button>
  </>
}

Enter fullscreen mode Exit fullscreen mode

The Final code in a single file will look like this

import { create } from "zustand";
import { Button } from "./components/button";
type State = {
  firstName: string,
  lastName: string,
  age: number,
}

type Action = {
  updateFirstName: (firstName: State['firstName']) => void;
  updateLastName: (lastName: State['lastName']) => void;
  updateAge: () => void;
}


const usePersonStore = create<State & Actions>((set) => ({
  firstName: '',
  lastName: '',
  age: 0,
  updateAge: () => set((state) => ({ age: state.age + 1 })),
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),

}))


const AgeComp = () => {
  const updateAge = usePersonStore((state) => state.updateAge)
  return <>
    <Button onClick={updateAge}>+</Button>
  </>
}

export default function App() {

  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)
  const age = usePersonStore((state) => state.age);
  return <>
    <label htmlFor="firstName">
      First Name
      <input type="text" name="firstName" id="firstName"
        onChange={(e) => updateFirstName(e.target.value)}
      />
    </label>

    <AgeComp />

    <p>
      Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong>
    </p>
  </>
}import { create } from "zustand";
import { Button } from "./components/button";
type State = {
  firstName: string,
  lastName: string,
  age: number,
}

type Action = {
  updateFirstName: (firstName: State['firstName']) => void;
  updateLastName: (lastName: State['lastName']) => void;
  updateAge: () => void;
}


const usePersonStore = create<State & Actions>((set) => ({
  firstName: '',
  lastName: '',
  age: 0,
  updateAge: () => set((state) => ({ age: state.age + 1 })),
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),

}))


const AgeComp = () => {
  const updateAge = usePersonStore((state) => state.updateAge)
  return <>
    <Button onClick={updateAge}>+</Button>
  </>
}

export default function App() {

  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)
  const age = usePersonStore((state) => state.age);
  return <>
    <label htmlFor="firstName">
      First Name
      <input type="text" name="firstName" id="firstName"
        onChange={(e) => updateFirstName(e.target.value)}
      />
    </label>

    <AgeComp />

    <p>
      Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong>
    </p>
  </>
}
Enter fullscreen mode Exit fullscreen mode

Creating Slices in zustand

This is to split up a store which can potentially scale up and become huge in the future. So we split up a huge store into slices to achieve modularity.

Creating types for the individual slices

Create a new file inside src/store/fish-bear-slice.ts and start adding the below code.
type BearSlice = {
  bears: number;
  addBear: () => void;
  eatFish: () => void;
  resetBears: () => void;
};

type FishSlice = {
  fishes: number;
  addFish: () => void;
  resetFishes: () => void;
};

type SharedSlice = {
  addBoth: () => void;
  getBoth: () => void;
  resetBoth: () => void;
};
Enter fullscreen mode Exit fullscreen mode

Creating the slices using StateCreator

const createBearSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
  resetBears: () => set(() => ({ bears: 0 })),
});

const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
  resetFishes: () => set(() => ({ fishes: 0 })),
});
Enter fullscreen mode Exit fullscreen mode

Creating a shared slice

Combining the above given two individual slices to one.This will let you use every single action and state used in the store as one yet manage all the slices modularly.

const createSharedSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  SharedSlice
> = (set, get) => ({
  addBoth: () => {
    get().addBear();
    get().addFish();
  },
  getBoth: () => get().bears + get().fishes,
  resetBoth: () => {
    get().resetBears();
    get().resetFishes();
  },
});
Enter fullscreen mode Exit fullscreen mode

Creating a combined Store using a custom hook.

We are exporting this custom hook so that we can use it in the react component.

export const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()(
  (...a) => ({
    ...createBearSlice(...a),
    ...createFishSlice(...a),
    ...createSharedSlice(...a),
  })
);
Enter fullscreen mode Exit fullscreen mode

Using the above created combined slice in React

import { useBoundStore } from "./store/fish-bear-slice"


const Controllers = () => {
  const addBear = useBoundStore((state) => state.addBear)
  const addFish = useBoundStore((state) => state.addFish)
  const addBoth = useBoundStore((state) => state.addBoth)
  const resetBoth = useBoundStore((state) => state.resetBoth)
  return <>
    <button onClick={() => addBear()}>Add a bear</button>
    <button onClick={() => addFish()}>Add a fish</button>
    <button onClick={() => addBoth()}>Add both</button>
    <button onClick={() => resetBoth()}>Reset Both</button>
  </>
}

function App() {
  const bears = useBoundStore((state) => state.bears)
  const fishes = useBoundStore((state) => state.fishes)

  return (
    <div>
      <h2>Number of bears: {bears}</h2>
      <h2>Number of fishes: {fishes}</h2>
      <Controllers />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Persisting the data inside the state using the persist middleware from zustand

The above code will work perfectly but once you refresh the page the data is not stored anywhere and will reset the data to its default value. To retain the data even after we reload we use the help of the 'persist' middleware from zustand and provide it with a unique localStorage name.

_Persist utilizes the localStorage to persist the data and zustand handles the saving and retrieving part of the job.
_

Import the persist from zustand/middleware

import { persist } from "zustand/middleware";

Enter fullscreen mode Exit fullscreen mode

Add the persist middleware to the combined slice.

Do not use it on individual slices since it will create a problem at the end.

export const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()(
  persist(
    (...a) => ({
      ...createBearSlice(...a),
      ...createFishSlice(...a),
      ...createSharedSlice(...a),
    }),

    { name: "bound-store" }
  )
);
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
stark-akshay
Akshay Manoj

Posted on August 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related