
Common mistakes regarding React rendering behavior
Posted on
As a react developer who builds a web app that can handle more than 50 WebSocket events that will be merged into state per second while keeping it 60 FPS, I spend a lot of times on tuning the performance on my web apps, and recently, when I was doing code review on a junior developer, I noticed some mistakes that will harm the performance of the web apps, I'll list some that are related to react rendering behavior:
- using multiple setState after calling an asynchronous function:
- pass an anonymous function as a prop on a React component using the memo function
- pass a constant created in a React Component as a prop on a child React component using the memo function
- (redux) dispatch multiple actions outside of React without using
batch()
Using multiple setState after calling an asynchronous function
function App() {
const [counter, setCounter] = useState(0);
const onClick = async () => {
setCounter(() => {
console.log("pass0", Date.now());
return 1;
});
setCounter(() => {
console.log("pass1", Date.now());
return 2;
});
// in reality, it will be something like a function that fetches some data
await sleep(1000);
setCounter(() => {
console.log("pass2", Date.now());
return 3;
});
setCounter(() => {
console.log("pass3", Date.now());
return 4;
});
};
return (
<div className="App">
<button onClick={onClick}>{counter}</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Can you tell how many times will React execute render passes? if you say setCounter(1) and setCounter(2) will be grouped, and have the same timestamp, after one second, comes with setCounter(3) and setCounter(4) to be batched, you get it half right.
The correct answer will be three times, setCounter(1) and setCounter(2) will be grouped, then setCounter(3), the last is setCounter(4).
The reason behind this is the first two setCounter will be batched together by React internally to reduce re-render time since it's in the same call stack, setCounter(3) and setCounter(4) are not in the original call stack, so React will re-render the component each time a setState function gets called.
But what if you need to calls multiple setState after an asynchronous function? you can use the unstable_batchedUpdates() from React to batch them manually.
pass an anonymous function as a prop on a React component using memo function
const DatePicker = React.memo(ChildComponent)
function App() {
return (
<DatePicker handleClick={(event) => setDateRange(event)}/>
);
}
Since handleClick is passed with an anonymous function, DatePicker will receive a new handleClick function every time the App component is rendered, make React.memo useless, and will suffer from performance.
pass a constant created in a React Component as a prop on a child React component using memo function
const DatePicker = React.memo(ChildComponent)
function App() {
const dateRange = {startTime: "yyyy-MM-dd", endTime: "yyyy-MM-dd"}
return (
<DatePicker dateRange ={dateRange }/>
);
}
Since dateRange it not wrapped in useMemo and is created in a React component, dateRange will be created every time the App component is rendered, make React.memo useless, and will suffer from performance.
4. (redux) dispatch multiple actions outside of React without using batch()
import { batch } from 'react-redux'
function myThunk() {
return (dispatch, getState) => {
batch(() => {
dispatch(updateDate())
dispatch(updateDate())
})
}
}
By using batch(), you can sure the result Is only one combined re-render, not two. It's a useful function if you are using react-redux to manage your state.
That's it, these are the mistake that I had made before, hope it helps!