DEV Community

timeturnback
timeturnback

Posted on

How to use reselect with zustand

Sometime , you may want to use a selector to pick up some comlicated calculation from the zustand store, in react , we have reselect to help us . But we can use reselect in zustand too.

First , let create a zustand store

import { create } from "zustand";

// Type
type BearData = {
  name: string;
  attitude: string;
  fishNeed: number;
};

type CatData = { name: string; attitude: string; fishNeed: number };

interface BearStoreState {
  fishes: number;
  bearsList: BearData[];
  catList: CatData[];
  addRandomBear: () => void;
  addRandomCat: () => void;
  addFish: () => void;
}

// Create the store with fish , cat and bears
const useBearStore = create<BearStoreState>((set) => ({
  fishes: 0,
  bearsList: [
    {
      name: "bear1",
      attitude: "angry",
      fishNeed: 2,
    },
  ],
  catList: [
    {
      name: "cat1",
      attitude: "angry",
      fishNeed: 2,
    },
  ],
  addRandomBear: () =>
    set((state) => ({
      bearsList: [
        ...state.bearsList,
        {
          name: `bear${state.bearsList.length + 1}`,
          attitude: Math.round(Math.random() * 100) % 2 === 0 ? "angry" : "happy",
          fishNeed: Math.round(Math.random() * 10),
        },
      ],
    })),

  addRandomCat: () =>
    set((state) => ({
      catList: [
        ...state.catList,
        {
          name: `cat${state.catList.length + 1}`,
          attitude: Math.round(Math.random() * 100) % 2 === 0 ? "angry" : "happy",
          fishNeed: Math.round(Math.random() * 10),
        },
      ],
    })),
  addFish: () => set((state) => ({ fishes: state.fishes + 5 })),
}));
Enter fullscreen mode Exit fullscreen mode

We will have a simple App to run this example

I will separate the component into 4 parts for you to observe the renders of the component when the store change
You can watch the render with react dev tool

const App = () => {
  return (
    <div>
      <FishInfo />
      <BearInfo />
      <CatInfo />
      <AngryAnimalsInfo />
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

FishInfo

const FishInfo = () => {
  const addFish = useBearStore((state) => state.addFish);
  const fishes = useBearStore((state) => state.fishes);

  return (
    <div>
      <div>Total fished : {fishes}</div>
      <button onClick={addFish}>Add fish</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

BearInfo

const BearInfo = () => {
  const addRandomBear = useBearStore((state) => state.addRandomBear);

  const angryBear = useBearStore(angryBearsSelect);

  return (
    <div>
      <button onClick={addRandomBear}>Add Bear</button>
      <h1>Angry bear</h1>
      {angryBear.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

CatInfo

const CatInfo = () => {
  const addRandomCat = useBearStore((state) => state.addRandomCat);
  const angryCat = useBearStore(angryCatReselect);

  return (
    <div>
      <button onClick={addRandomCat}>Add Cat</button>

      <h1>Angry cat</h1>
      {angryCat.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

AngryAnimalsInfo

const AngryAnimalsInfo = () => {
  const angryAnimals = useBearStore(angryAnimalsReselect);

  return (
    <div>
      <h1>Angry animals</h1>
      {angryAnimals.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Finally . The selector .

I put the console.log here for you to observe how much time the selector run when the store change

const angryCatReselect = createSelector(
  (state: BearStoreState) => state.catList,
  (catList) => {
    console.log("angryCatReselect");
    return catList.filter((cat) => cat.attitude === "angry");
  }
);

const angryAnimalsReselect = createSelector(
  (state: BearStoreState) => state.bearsList,
  (state: BearStoreState) => state.catList,
  (bearsList, catList) => {
    console.log("angryAnimalsReselect");
    return [...bearsList, ...catList].filter((animal) => animal.attitude === "angry");
  }
);

const angryBearsSelect = (state: BearStoreState) => {
  console.log("angryBearsSelect");
  return state.bearsList.filter((bear) => bear.attitude === "angry");
};
Enter fullscreen mode Exit fullscreen mode

Without reselect . The selector will run every time the store change . But with reselect . The selector will run only when the state that it depend on change

As you can see here , with angryBearsSelect , it will re-render everytime when you addFish, addRandomBear , addRandomCat . But with angryCatReselect , it will only re-render when you addRandomCat.

I put angryAnimalsReselect here as a example for multiple state selector . It will re-render when you addRandomBear , addRandomCat , not addFish

Put it all together

import { createSelector } from "reselect";
import { create } from "zustand";

type BearData = {
  name: string;
  attitude: string;
  fishNeed: number;
};
type CatData = { name: string; attitude: string; fishNeed: number };
interface BearStoreState {
  fishes: number;
  bearsList: BearData[];
  catList: CatData[];
  addRandomBear: () => void;
  addRandomCat: () => void;
  addFish: () => void;
}
const useBearStore = create<BearStoreState>((set) => ({
  fishes: 0,
  bearsList: [
    {
      name: "bear1",
      attitude: "angry",
      fishNeed: 2,
    },
  ],
  catList: [
    {
      name: "cat1",
      attitude: "angry",
      fishNeed: 2,
    },
  ],
  addRandomBear: () =>
    set((state) => ({
      bearsList: [
        ...state.bearsList,
        {
          name: `bear${state.bearsList.length + 1}`,
          attitude: Math.round(Math.random() * 100) % 2 === 0 ? "angry" : "happy",
          fishNeed: Math.round(Math.random() * 10),
        },
      ],
    })),

  addRandomCat: () =>
    set((state) => ({
      catList: [
        ...state.catList,
        {
          name: `cat${state.catList.length + 1}`,
          attitude: Math.round(Math.random() * 100) % 2 === 0 ? "angry" : "happy",
          fishNeed: Math.round(Math.random() * 10),
        },
      ],
    })),
  addFish: () => set((state) => ({ fishes: state.fishes + 5 })),
}));

const angryCatReselect = createSelector(
  (state: BearStoreState) => state.catList,
  (catList) => {
    console.log("angryCatReselect");
    return catList.filter((cat) => cat.attitude === "angry");
  }
);

const angryAnimalsReselect = createSelector(
  (state: BearStoreState) => state.bearsList,
  (state: BearStoreState) => state.catList,
  (bearsList, catList) => {
    console.log("angryAnimalsReselect");
    return [...bearsList, ...catList].filter((animal) => animal.attitude === "angry");
  }
);

const angryBearsSelect = (state: BearStoreState) => {
  console.log("angryBearsSelect");
  return state.bearsList.filter((bear) => bear.attitude === "angry");
};

const App = () => {
  return (
    <div>
      <FishInfo />
      <BearInfo />
      <CatInfo />
      <AngryAnimalsInfo />
    </div>
  );
};

const FishInfo = () => {
  const addFish = useBearStore((state) => state.addFish);
  const fishes = useBearStore((state) => state.fishes);

  return (
    <div>
      <div>Total fished : {fishes}</div>
      <button onClick={addFish}>Add fish</button>
    </div>
  );
};

const BearInfo = () => {
  const addRandomBear = useBearStore((state) => state.addRandomBear);

  const angryBear = useBearStore(angryBearsSelect);

  return (
    <div>
      <button onClick={addRandomBear}>Add Bear</button>
      <h1>Angry bear</h1>
      {angryBear.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};

const CatInfo = () => {
  const addRandomCat = useBearStore((state) => state.addRandomCat);
  const angryCat = useBearStore(angryCatReselect);

  return (
    <div>
      <button onClick={addRandomCat}>Add Cat</button>

      <h1>Angry cat</h1>
      {angryCat.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};

const AngryAnimalsInfo = () => {
  const angryAnimals = useBearStore(angryAnimalsReselect);

  return (
    <div>
      <h1>Angry animals</h1>
      {angryAnimals.map((item) => (
        <div key={item.name}>
          {item.name} - Need : {item.fishNeed}
        </div>
      ))}
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Some observation

  • When add fish , only FishInfo should re-render . But here . BearInfo re-render too . That is because we use angryBearsSelect to display list of angry bear , angryBearsSelect will trigger everytime state is change .

We actually can eliminate the render of BearInfo by using compare function . But , as you check the log of angryBearsSelect , the calculation will still run . So , it is better to use reselect to eliminate the calculation too .

const angryBear = useBearStore(angryBearsSelect, compare);

const compare = (prev: any, next: any) => {
  if (prev.length !== next.length) return false;
  for (let i = 0; i < prev.length; i += 1) {
    if (!isEqual(prev[i].id, next[i].id)) return false; // isEqual is come from lodash
  }
  return true;
};
Enter fullscreen mode Exit fullscreen mode
  • CatInfo only be re-render when addRandomCat is called

  • AngryAnimalsInfo only be re-render when addRandomBear or addRandomCat is called . Of course . And it wont be re-render when you addFish

So that how we use re-render for zustands .

Top comments (0)