0

I'm trying to fully understand the relations among DOM reconciliation, virtual DOM, component rendering and effect hook. I'm using the following code, which returns a status of "three". But are the intermediate statuses "one" and "two" always printed on the DOM, even for a blinky fraction of a second?

export default function App() {
  const [status, setStatus] = React.useState("zero");

  const handleChangeStatus = () => {
    setStatus("one");
  };

  useEffect(() => {
    if (status === "one") {
      setStatus("two");
    }
  }, [status]);

  useEffect(() => {
    if (status === "two") {
      setStatus("three");
    }
  }, [status]);

  return (
    <div>
      <div id={status}>{status}</div>
      <br />
      <button onClick={handleChangeStatus}>Change statuses</button>
    </div>
  );
}

once clicked,

Bertuz
  • 2,390
  • 3
  • 25
  • 50
  • Open chrome dev tools, find the div node in inspector, right-click on this div element and enable Break on Subtree Modifications. You will be able to see "one" "two" "three" clicking "continue" button – Sergey Sosunov Aug 26 '22 at 23:31
  • actually I've tried it, but it's quite a bit empirical! Are we sure it is a deterministic rule? Actually the central point of the question could be: is useEffect with a variable watch called always _after_ the DOM has been updated, and hence a render has been called + virtual dom updated + DOM reconciliated? – Bertuz Aug 26 '22 at 23:35
  • You should check this [answer](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973) – Sergey Sosunov Aug 26 '22 at 23:40
  • Based on the [React docs](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous), "React may batch multiple setState() calls into a single update for performance.". The word "may" makes me think this is not a deterministic rule. But see @Bertuz 's comment – Andrew Hulterstrom Aug 26 '22 at 23:40
  • @AndrewHulterstrom Yes, but setState is async operation, it does not updates the value immediatelly. So state changed from "zero" to "one" with click - all 3 useEffects executed, only one of them changed the value to next but "next" will appear on next render only, on next execution of the component function. – Sergey Sosunov Aug 26 '22 at 23:46
  • @SergeySosunov It seems to be a common misconception/misunderstanding that "setState is async operation". It's not. The `setState` function is a completely synchronous function. It's the enqueued state update that asynchronously processed by React later. Updating React states is the asynchronous operation. – Drew Reese Aug 27 '22 at 01:24

1 Answers1

2

Simply put, the useEffect hook runs at the end of the render cycle after content has been flushed to the DOM. Each useEffect hook runs each render cycle in the order they are declared. Each useEffect hook enqueues a state update on a different render cycle based on the dependency's value in the effect callback.

But are the intermediate statuses "one" and "two" always printed on the DOM, even for a blinky fraction of a second?

Yes, they are. Each state update is enqueued in a different render cycle, so each update will be processed and the reconciled diff flushed to the DOM and rendered. The React framework aims for about a 60fps refresh rate, or, about 17ms per render cycle. The basic math: 1s/60render * 1000ms/1s = 1000ms/60render = 16.6667ms/render. 17ms is nearly imperceptible I dare say.

If you take this into account and multiply by the 3 triggered state updates, we're talking about around 50ms for just React doing the work. That's still nearly imperceptible. For reference, it seems the average time for a human to blink is between 100-150ms.

You can also very easily test this out for yourself with a couple extra useEffect hooks logging out the state updates and render cycles. Take note of the timestamps in the log output.

function App() {
  const [status, setStatus] = React.useState("zero");

  const handleChangeStatus = () => {
    setStatus("one");
  };

  React.useEffect(() => {
    if (status === "one") {
      setStatus("two");
    }
  }, [status]);

  React.useEffect(() => {
    if (status === "two") {
      setStatus("three");
    }
  }, [status]);

  React.useEffect(() => {
    console.log("Status updated", status);
  }, [status]);

  React.useEffect(() => {
    console.log("Component rendered")
  })

  return (
    <div>
      <div id={status}>{status}</div>
      <button onClick={handleChangeStatus}>Change statuses</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root" />
Drew Reese
  • 165,259
  • 14
  • 153
  • 181