Introduction
setTimeout
is a very commonly used JavaScript function. It is very useful for many things, such as timing how long an alert should show, or debouncing a button to prevent double clicks.
However when using higher level rendering frameworks like React, Angular, Vue etc, it can give you control over the JavaScript runtime itself to make your life easier!
A simple problem
Let's take this simple app as an example:
export default function App() {
const [text, setText] = useState("This is some text");
const [pWidth, setPWidth] = useState(0);
const pRef = useRef();
const onTextChange = (changedText) => {
setText(changedText);
setPWidth(Math.round(pRef.current.getBoundingClientRect().width));
};
return (
<>
<div>
<input value={text} onChange={(ev) => onTextChange(ev.target.value)} />
</div>
<p ref={pRef} style={{ display: "inline" }}>{text}</p>
<p>{pWidth}</p>
</>
);
}
This app contains an input
that when changed, runs a function to update the text
state variable and uses pRef
to extract the current width of the p
element and set pWidth
.
This renders the following:
pWidth
is only going to be set when we change the text, so lets delete the text and we should see a width of 0
:
Huh? That's not right! It's showing the width of the p
element when it had text in it!
This is because when we update text
via setText
, React hasn't had a chance to set the text in the p
element, meaning the width of the p
element hasn't changed before we set its width using setPWidth
!
setTimeout to the rescue!
We can fix this using setTimeout!
Let's change this line:
setPWidth(Math.round(pRef.current.getBoundingClientRect().width));
To this:
setTimeout(() => {
setPWidth(Math.round(pRef.current.getBoundingClientRect().width));
});
And check out output when we clear the input:
And like magic it works as expected!
Why does this work?
As we have already ascertained, the issue was that we needed to wait until React had actually rendered the text in the p
element before setting the p
elements width.
setTimeout
solves this problem by deferring setting the width of the p
element to the next JavaScript runtime cycle, ensuring that the React has rendered the changes from the previous runtime cycle before running our pending setTimeout
call-back function.
Using async/await
This technique can also be used in async functions with the use of a utility function such as:
const waitForRender = () => new Promise(res => setTimeout(res));
Using the example above, waitForRender
could be used like this:
const onTextChange = async (changedText) => {
setText(changedText);
await waitForRender();
setPWidth(pRef.current.getBoundingClientRect().width);
};
This produces the same output!
Conclusion
That's it! I hope this helped you learn a new technique to add to your toolset when tackling edge case problems that require waiting for a rendering cycle to pass.
Follow me on Twitter to keep up to date with my projects and get notified of new articles like this in the future!