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)