'React: why is that changing the current value of ref from useRef doesn't trigger the useEffect here

I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.

for example:

export default function App() {
  const myRef = useRef(1);
  useEffect(() => {
    console.log("myRef current changed"); // this only gets triggered when the component mounts
  }, [myRef.current]);
  return (
    <div className="App">
      <button
        onClick={() => {
          myRef.current = myRef.current + 1;
          console.log("myRef.current", myRef.current);
        }}
      >
        change ref
      </button>
    </div>
  );
}

Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?

Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like

 const myRef = useRef(1);
  useEffect(() => {
    //...
  }, [myRef]);

I am putting the current value in the dep list so that should be changing.



Solution 1:[1]

I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.

Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?

Short answer, no.

The only things that cause a re-render in React are the following:

  1. A state change within the component (via the useState or useReducer hooks)
  2. A prop change
  3. A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)

Let's see what happens in the code example you shared:

export default function App() {
  const myRef = useRef(1);
  useEffect(() => {
    console.log("myRef current changed"); // this only gets triggered when the component mounts
  }, [myRef.current]);
  return (
    <div className="App">
      <button
        onClick={() => {
          myRef.current = myRef.current + 1;
          console.log("myRef.current", myRef.current);
        }}
      >
        change ref
      </button>
    </div>
  );
}

Initial render

  • myRef gets set to {current: 1}
  • The effect callback function gets registered
  • React elements get rendered
  • React flushes to the DOM (this is the part where you see the result on the screen)
  • The effect callback function gets executed, "myRef current changed" gets printed in the console

And that's it. None of the above 3 conditions is satisfied, so no more rerenders.

But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.

If the component was to rerender now (say, due to its parent rerendering), the following would happen:

Normal render

  • myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
  • The previous effect's cleanup function gets executed (here there is none)
  • The effect callback function gets registered
  • React elements get rendered
  • React flushes to the DOM if necessary
  • The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)

All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.

Solution 2:[2]

https://reactjs.org/docs/hooks-reference.html#useref

Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.

Solution 3:[3]

use useCallBack instead, here is the explanation from React docs:

We didn’t choose useRef in this example because an object ref doesn’t notify us about changes to the current ref value. Using a callback ref ensures that even if a child component displays the measured node later (e.g. in response to a click), we still get notified about it in the parent component and can update the measurements.

Note that we pass [] as a dependency array to useCallback. This ensures that our ref callback doesn’t change between the re-renders, and so React won’t call it unnecessarily.

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

Solution 4:[4]

Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:

export default function App() {
  const [x, setX] = useState();
  const myRef = useRef(1);
  useEffect(() => {
    console.log("myRef current changed"); // this only gets triggered when the component mounts
  }, [myRef.current]);
  return (
    <button
        onClick={() => {
          myRef.current = myRef.current + 1;
          // Update state too, to trigger a re-render
          setX(Math.random());
          console.log("myRef.current", myRef.current);
        }}
      >
        change ref
      </button>
  );
}

Now you can see it will trigger the effect.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Damian Cyntler
Solution 3 Gal Margalit
Solution 4