React useState callback function

What is the callback function in a useState for?

Friday, October 22, 2021

lazy cat

Why do we need to pass a callback function to the useState function

TL;DR version

We can pass a callback function when calling useState to take advantage of lazy initialization.

This will resolve the source of the state only once, thus avoiding unnecessary computation during re-renders.

Longer version

When we develop real production applications, we do not always derive our state from simple primitive values. Sometimes we need to calculate them based on other values, as shown below.

.jsx
12const [state1, setState1] = useState(prop1 === prop2 && prop2 !== prop3 && prop1 < prop3)const [state2, setState2] = useState(prop4 === prop5 && prop5 !== prop6 && prop4 < prop6)

You might notice the logics to get the default value are very identical so as a good programmer, we can extract the logic to a new function to avoid duplication.

.jsx
1234567function reusableFunctionToGetState(first, second, third) { return first === second && second !== third && first < third} // We call the function with the appropriate arguments. const [state1, setState1] = useState(reusableFunctionToGetState(prop1, prop2, prop3));const [state2, setState2] = useState(reusableFunctionToGetState(prop4, prop5, prop6));

The problem occurs when the source of our state is computationally expensive. Every time we re-render our component, we need to re-evaluate our default state. Try to see the problem by playing with the code below.

.jsx
12345678910function UseStateDemo() { const complexFunction = () => { // Something computationally expensive return 'result of a heavy task' } const [,setState] = React.useState(complexFunction()) return <button onClick={() => setState(Date.now())} >Click Me</button>}
Link to Codepen

Each time we click the button, the complexFunction will always be called(verify by uncommenting console.log or alert). If that complexFunction is computationally expensive, then expect our component will render slower.


Solution: perform a lazy initialization

For a such big word like lazy initialization it actually is very simple. We simply need pass a function instead of a value as shown below.

.jsx
123const [,setState] = React.useState(() => complexFunction())// ORconst [,setState] = React.useState(complexFunction)

Try to play with the code below and see the difference

.jsx
1234567891011function SetStateDemo() { const complexFunction = () => { // Something computationally expensive return 'result of a heavy task' } const [,setState] = React.useState(() => complexFunction) // const [,setState] = React.useState(complexFunction) // Alternatively return <button onClick={() => setState(Date.now())} >Click Me</button>}
Link to Codepen

The complexFunction will evaluate only once if we pass it as a function and not invoking it immediately, thus giving us a performance boost.


Do we need to always lazy initialize our state?

No. It's not magic that allows lazy initialization to do the above optimization. There's an overhead to it which means if we use it everywhere, it could make our component "slower" in contrast to our goal to make it faster.



Conclusion

Use lazy initialization when your state depends on something that is computationally heavy. Avoid unnecessary "optimization"; lazy initialization has a small overhead, so use it wisely.