How does React update ref?

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [minus, setMinus] = useState(1);
  const ref = useRef(null);

  const handleClick = () => {
    setMinus(minus - 1);
  };

  console.log("-->", ref.current, minus);

  useEffect(() => {
    console.log(`denp[minus]>`, ref.current);
  }, [minus]);

  return (
    <div className="App">
      {(minus >= 0 || minus <= -5) && <h1 ref={ref}>num: {minus}</h1>}
      <button onClick={handleClick}>minus</button>
    </div>
  );
}

export default App;

In the official doc, it says "React will set its .current property to the corresponding DOM node whenever that node changes."

So after React renders DOM, the DOM node changes and React updates ref. However, React does not seem to work like that.

In the first render, the first console.log logs out ref is null

I press the button, state minus is updated to 0

In the second render, the first console.log logs out ref is <h1>num: 0</h1>. But following the doc, I expect to get <h1>num: 1</h1> because the ref is only updated after the DOM node updates

I press the button, state minus is updated to -1

In the third render, the first console.log logs out ref: <h1>num: 0</h1>. Now it works like the doc. So how does React update ref?

Live code here: https://codesandbox.io/s/quirky-architecture-r7wvr0

>Solution :

You’re getting tripped up by a quirk of the way console.log works. When you pass an object to console.log, the browser does not immediately turn it into a string, then log that out. Instead, it saves a reference to the object, and if you inspect the object in your console at some later point in time, it will evaluate the object at the time you click in the console. If the object has been mutated between when it was logged and when you clicked, you will see the new version.

So the first render happens, and you run console.log("-->", ref.current, minus);, which results in null. Then you continue the render process, and return the elements you want, and react puts them on the dom. Now that the elements are on the dom, react updates ref.current.

Then the second render happens, and you log out ref.current, which contains the element from the first render. At this exact moment, the inner text is 1, but you can’t possibly click at this instant. Rendering continues, you return that you want to change it to 0, and react obliges, updating the dom. You then go into your console and click on the log. The inner text is 0 by this time, so that’s what you see.

Then the third render happens, and you log out ref.current. Again, it’s the element from the first render, and it’s inner text is currently 0 due to the most recent render. You return that you want to unmount the element, so react removes it from the dom. Now that rendering is done, react updates ref.current to null. You go into your console and click the log. The element has been removed from the page, but it’s still in memory and hasn’t been changed, so you see an inner text of 0.


If you’d like to get rid of the complication that logging objects is adding, try logging a string instead so it has to be evaluated immediately. For example: console.log("-->", ref.current?.innerHTML, minus);

Leave a Reply