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 })),
}));
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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");
};
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;
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;
};
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)