React - Avoid Unnecessary Re-Rendering of Array Components When Using Zustand
If you have a medium.com membership, I would appreciate it if you read this article on medium.com instead to support me~ Thank You! 🚀
Intro
This article is mainly a recap for myself as well as to share how to re-render an array of components efficiently with React and Zustand. It will cover a simple optimization that you can make to avoid re-rendering all your array components when only 1 of the component is updated.
Note: This article is based on using Zustand as my state management library.
Context
const defaultFlags = [
{ name: "Flag A", enabled: false },
{ name: "Flag B", enabled: false },
{ name: "Flag C", enabled: false },
];
const App = () => {
const [flags, setFlags] = useState(defaultFlags);
return flags.map((flag, idx) => (
<div key={idx}>
<input
type='checkbox'
checked={flag.enabled}
onChange={(event) => {
const tempFlags = [...flags];
tempFlags[idx] = {
...tempFlags[idx],
enabled: event.currentTarget.checked,
};
setFlags(tempFlags);
}}
/>
<span>{flag.name}</span>
</div>
));
};
This is a very common use case in React where you are rendering components from an array of objects. In my example, I am rendering an array of feature flags that can be checked on and off using the checkbox. If we were to use Zustand to manage the feature flag state instead, the implementation will be as follows:
// initial flag state
const defaultFlags = [
{ name: "Flag A", enabled: false },
{ name: "Flag B", enabled: false },
{ name: "Flag C", enabled: false },
];
// zustand store
const useFeatureFlagStore = create((set) => ({
flags: [],
toggleFlag: (flagIdx, enabled) => {
set((state) => {
const tempFlags = { ...state.flags };
tempFlags[flagIdx] = {
...tempFlags[flagIdx],
enabled: enabled,
};
return { flags: tempFlags };
});
},
initFlags: (flags) => {
set(() => ({ flags: flags }));
},
}));
// feature flag switch component
const FeatureFlagSwitch = ({ flag, idx }) => {
const toggleFlag = useFeatureFlagStore((state) => state.toggleFlag);
console.log(`Rendering Switch: Flag = ${flag.name} Enabled = ${flag.enabled}`);
return (
<div>
<input
type='checkbox'
checked={flag.enabled}
onChange={(event) => toggleFlag(idx, event.currentTarget.checked)}
/>
<span>{flag.name}</span>
</div>
);
};
// feature flag parent component
const FeatureFlagsContainer = () => {
const flags = useFeatureFlagStore((state) => state.flags);
return Object.keys(flags).map((flag, idx) => {
const featureFlag = flags[flag];
return <FeatureFlagSwitch key={idx} flag={featureFlag} idx={idx} />;
});
};
// main app
const App = () => {
const initFlags = useFeatureFlagStore((state) => state.initFlags);
initFlags(defaultFlags);
return <FeatureFlagsContainer />;
};
TLDR: We have a Zustand store which we initialize on the first start-up (Note: this is just a demo, the initFlag
function will be called whenever App
components re-renders, but you get the idea). The store contains the feature flag states and allows us to toggle the feature flag status globally.
The issue here is that all FeatureFlagSwitch
components are updated whenever any checkbox is checked or unchecked. Hence, let’s take a look at how we can optimize it with some simple changes in the codes.
Time for Optimization
Instead of re-rendering all components in the array, we will only re-render the relevant component in the array. In our example, that component will be the FeatureFlagSwitch
. Here are some of the key changes to make.
#1 Using Zustand Shallow Diff
If we refer to Zustand documentation, instead of comparing the entire object, we can compare the keys to determine the need for re-rendering. This is as simple as passing the shallow
equality function. Here’s what it looks like in codes:
const FeatureFlagsContainer = () => {
const flags = useFeatureFlagStore((state) => Object.keys(state.flags), shallow);
return flags.map((flag, idx) => {
return <FeatureFlagSwitch key={idx} flag={flag} />;
});
};
With this, the variable flags
will not trigger a re-render to the parent component FeatureFlagsContainer
unless there is a change to the order, count, or keys.
#2 Using Zustand State Instead of Props Drilling
Instead of passing the FeatureFlag
props from the FeatureFlagsContainer
component (parent) to each FeatureFlagSwitch
component (child). Zustand provides an easy way for us to read our states as shown below:
const FeatureFlagSwitch = ({ flag }) => {
const featureFlag = useFeatureFlagStore((state) => state.flags[flag]);
const toggleFlag = useFeatureFlagStore((state) => state.toggleFlag);
return (
<div>
<input
type='checkbox'
checked={featureFlag.enabled}
onChange={(event) => toggleFlag(featureFlag.name, event.currentTarget.checked)}
/>
<span>{featureFlag.name}</span>
</div>
);
};
#3 Using Immer for Nested Objects
I find Immer to be a really handy tool when it comes to modifying objects. Let’s compare the following codes:
// BEFORE
const useFeatureFlagStore = create((set) => ({
flags: {},
toggleFlag: (flagIdx, enabled) => {
set((state) => {
const tempFlags = { ...state.flags };
tempFlags[flagIdx] = {
...tempFlags[flagIdx],
enabled: enabled,
};
return { flags: tempFlags };
});
},
}));
// AFTER
const useFeatureFlagStore = create(
immer((set) => ({
flags: {},
toggleFlag: (flagIdx, enabled) => {
set((state) => {
state.flags[flagIdx].enabled = enabled;
});
},
}))
);
In React, we have to update the state immutably. This means that we have to create a copy of the whole state (Eg. tempFlags
) and update the values we want to change. With Immer, it simplifies the handling of immutable data structures as shown in the codes above. For more information, check out Immer documentation.
Here are the full codes
Lastly, these are the code snippets for javascript. If you need the typescript version, refer to the repository below.
// initial flag state
const defaultFlags = [
{ name: "Flag A", enabled: false },
{ name: "Flag B", enabled: false },
{ name: "Flag C", enabled: false },
];
// zustand store
const useFeatureFlagStore = create(
immer((set) => ({
flags: [],
toggleFlag: (flagIdx, enabled) => {
set((state) => {
state.flags[flagIdx].enabled = enabled;
});
},
initFlags: (flags) => {
set((state) => {
state.flags = flags;
});
},
}))
);
// feature flag switch component
const FeatureFlagSwitch = ({ flag, idx }) => {
const featureFlag = useFeatureFlagStore((state) => state.flags[flag]);
const toggleFlag = useFeatureFlagStore((state) => state.toggleFlag);
console.log(`Rendering Switch: Flag = ${featureFlag.name} Enabled = ${featureFlag.enabled}`);
return (
<div>
<input
type='checkbox'
checked={featureFlag.enabled}
onChange={(event) => toggleFlag(idx, event.currentTarget.checked)}
/>
<span>{featureFlag.name}</span>
</div>
);
};
// feature flag parent component
const FeatureFlagsContainer = () => {
const flags = useFeatureFlagStore((state) => Object.keys(state.flags), shallow);
return flags.map((flag, idx) => {
return <FeatureFlagSwitch key={idx} flag={flag} idx={idx} />;
});
};
// main app
const App = () => {
const initFlags = useFeatureFlagStore((state) => state.initFlags);
initFlags(defaultFlags);
return <FeatureFlagsContainer />;
};
Summary
That’s it! This isn’t something very complicated or revolutionary. It is mainly a refresher for beginners working with React and Zustand, so I hope it’s as useful to you as much as it was to me.
Also, if you are new to Zustand, do check out this article “Working with Zustand” written by Dominik Dorfmeister. I find that it was really useful in helping me to understand Zustand.
Refer to the Git Repo below for the code reference
Thank you for reading till the end! ☕
If you enjoyed this article and would like to support my work, feel free to buy me a coffee on Ko-fi. Your support helps me keep creating, and I truly appreciate it! 🙏