DEV Community

TusharShahi
TusharShahi

Posted on • Edited on

Is React un-optimized?

I recently saw a question in StackOverflow which had the below code snippet:

export default function Mycomponent() {
  const [number,setNumber] =useState(0);
  const [string,setstring] =useState('');

  function func(event){
    setstring(event.target.value);
    setTimeout(()=>{
      setNumber(number+1);
    },3000);   
  }
  console.log(number,string);

  return (
    <>
      <input type='text'onChange={(e)=>{func(e)}} value={string}/>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

The user was confused with the log output:

Image description

If like me, your initial thought was that the output should have 1 'abcd' only once, then this post might be useful for you.

Multiple re-renders

Suppose the user starts entering a,b,c,d sequentially into the input. It goes without saying that every character typed will set a timeout. So setTimeout will be called 4 times. The callback in the timeout will be scheduled to run after atleast 3seconds. So there will be multiple setNumber calls, which will further lead to re-renders.

setstring changes the value of the input. After 3s, setNumber is executed with argument 1. So 1 abcd is printed for the first time. There will be further calls to setNumber due to the multiple timeouts set, but all will call setNumber(1). What is confusing is why the log shows 1 'abcd' twice. Calling setNumber(1) multiple times should cause only one re-render.

Here is a link of the sandbox

Is React un-optimized?

The above behaviour is not specific to setTimeout and is a detail of React. It can be observed even without a setTimeout.

function App() {
  const [number, setNumber] = useState(0);
  const [string, setstring] = useState("");
  function func(event) {
    setNumber(1);
  }
  console.log(number, string);
  return (
    <>
      <input
        type="text"
        onChange={(e) => {
          func(e);
        }}
        value={string}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The output for the above is:

Image description

Even when calling setNumber twice with the same value, React causes two renders. But yes, not a third one. Here is a link to sandbox

Does this mean React is unoptimized and causes re-render for the same value always?

React bailing behaviour

No, React does bail out of multiple re-renders in most cases. This works when we write setNumber twice in the same flow like the below.

import { useState } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";

function App() {
  const [number,setNumber] =useState(0);
  const [string,setstring] =useState('');
  function func(event){
    setstring(event.target.value);
    setNumber(number+1);
    setNumber(number+1);
  }
  console.log(number,string);
  return (
    <>
      <input type='text'onChange={(e)=>{func(e)}} value={string}/>
    </>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The output for the above is:

Image description

Here is the link to the sandbox

As expected, calling multiple setStates is not causing multiple re-renders.

But in the original example, set state is called from different contexts (from different setTimeout callbacks) and react still re-renders the component at least once. It does bail out from re-rendring the children though and that is how React creates an optimised User interface.

This is documented in the React docs. React can bail out of renders if the previous and new state values are same, but it might have to re-render the current component. It will definitely bail our for children though and that is why, this not a performance issue. From the docs:

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree.

The code snippet below shows the same behaviour:


const TestChild = () => {
  console.log("rerender");
  return null;
};


export default function App() {
  const [number, setnumber] = useState(0);
  const [string, setstring] = useState("");
  function func(event) {
    setstring(event.target.value);
    setTimeout(() => {
      setnumber(number + 1);
    }, 3000);
  }
  console.log(number, string);
  return (
    <>
      <TestChild />
      <input
        type="text"
        onChange={(e) => {
          func(e);
        }}
        value={string}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The output to the above is:

Image description

which clearly shows that TestChild is not rendered more than it should.

Here is a link to the sandbox

Notes

It is worth noting that a re-render does not mean re-paint of the screen. Repaint is only done when there is an actual diff between the two Virtual DOMs created. Render and commits are different and this gets even more important with concurrent React.

Top comments (0)