Use setTimeout in React to Defer Logic

Use setTimeout in React to Defer Logic

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:

image.png

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:

test.gif

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:

test.gif

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!