React Execute Code Immediately After Set State Update and Re-render
In some cases, after changing or updating the state of a component, we want to immediately execute or run a piece of code that not only requires the state to be updated, but also requires the re-render to have happened based on the new state. Such requirement occurs when after updating the state of a component, you want to execute some piece of code or function that is dependant on the updated or re-rendered DOM.
For example, after a this.setState()
(Class component) or setState()
(Functional component) call and its subsequent re-render, you may want to:
- Use the new values of
someEl.getBoundingClientRect()
. - Use the new values of
window.getComputedStyle(someEl)
. - Do some work based on some new DOM node/element that was added (for instance scroll to that element).
- Toggle a state value itself, from
true -> false
orfalse -> true
(after the re-render). - … These are a few examples that came to my mind but there could be thousands of such use cases.
Surely a setState()
call followed by our code doesn’t guarantee the fact that the following code will run after the state update operation and re-rendering has happened. This is because updating state in React is an asynchronous operation. This means any code that follows a setState()
call is executed before the state itself is actually updated and causes the re-render.
This is what we want to solve for. We want to run a piece of code not just after a state update call but after the call to state update has taken effect plus re-render has occurred.
Functional Components
With Functional components the way we do state management is by using the useState
hook. Let’s go through a real simple counter example.
import { useState } from 'react';
function ChildComponent(props) {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(document.querySelector('#count').innerHTML);
};
return (
<div>
<span id="count">{count}</span>
<button onClick={handleClick}>Click</button>
</div>
);
}
When the button
is clicked, it updates the count
stateful value. But the console.log
inside handleClick
that reads the count value from HTML/DOM will always log the old value (or old state value) and not the count + 1
value. What this means is any DOM access will be stale because running setState
does not lead to immediate state update (and hence re-render). This is because as discussed earlier, states are updated asynchronously and hence re-rendering is also asynchronous (and delayed).
What are our solutions ?
flushSync
One way to solve for this is by using the ReactDOM.flushSync
API.
// ...
import { flushSync } from 'react-dom';
function ChildComponent(props) {
// ...
const handleClick = () => {
flushSync(() => {
setCount(count + 1);
})
console.log(document.querySelector('#count').innerHTML);
};
// ...
}
The flushSync()
API method accepts a callback which can contain our state update logic. Any updates happening inside the callback will be flushed to the DOM synchronously. This means any code following the flushSync()
call will be able to immediately read the result of the updates that happened inside its callback.
In the example above, the setCount()
will update the count and the updates or changes will be flushed to the DOM due to flushSync()
causing the re-render before React moves on to the following code which is the console.log
line. This time the updated innerHTML
value will be read and logged.
Do note that abusing flushSync
by overusing it is not a good idea because flushing all changes to the DOM multiples times will have significant impact on the performance. So for instance make sure instead of using flushSync
and reading data multiple times in a single flow of execution, you batch all your updates in a single flushSync and then do the reading.
setTimeout
I’ve noticed some people use the setTimeout
hack to solve for this problem as well. In this case the code that requires the updated value is wrapped inside a setTimeout
with 0
as the delay:
const handleClick = () => {
setCount(count + 1);
setTimeout(() => {
console.log(document.querySelector('#count').innerHTML);
}, 0);
};
Although this works just fine, it might just make sense to use flushSync()
anyway because although setTimeout
will also run after entire changes are flushed to the dom, i.e., React has performed its re-render, it could be comparatively more “delayed” than flushSync()
depending upon when the browser picks up the timer callback and executes it (event loop!).
useEffect or useLayoutEffect
Surely useEffect
or useLayoutEffect
can also be used in this situation. You’ll choose one of them depending upon your situation. For our simple case (and most others), we can go ahead and use useEffect()
:
const handleClick = () => {
setCount(count + 1);
};
useEffect(() => {
console.log(document.querySelector('#count').innerHTML);
}, [count]);
The console.log
will dump output everytime the component re-renders due to state update. Notice how I passed the count
state as dependancy so that the effect callback only executes on re-renders when specifically the count
state changes, and not on any random re-render. In this simple case the re-render only happens when the count
state changes but in larger apps, your component will re-render due to multiple state variables and in those cases specifying the relevant state variable in the dependancy list will be super useful.
Class Components
With class components, our options are much simpler and straightforward compared to what we saw earlier for Functional components. The setState()
method already allows us to pass a callback function (second argument) that runs once setState
is done with updating the state and the component has re-rendered.
class ChildComponent extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick() {
// Notice how we pass the second callback to setState
this.setState((state, props) => {
return { count: state.count + 1 };
}, () => {
console.log(document.querySelector('#count').innerHTML);
});
}
render() {
return (
<div>
<span id="count">{this.state.count}</span>
<button onClick={this.handleClick.bind(this)}>Click</button>
</div>
);
}
}
Note how we’re passing the second callback to setState
above. However, it is recommended to perform this operation inside componentDidUpdate()
lifecycle method which is very similar to the useEffect
solution that we discussed earlier for Functional components.
componentDidUpdate() {
console.log(document.querySelector('#count').innerHTML);
}
Hope that helps!