Search code examples
reactjs

React: Changing `useRef.current` value of ref not triggering useEffect


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

  • 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 effect callback function gets registered
    • React elements get rendered
    • React flushes to the DOM if necessary
    • The previous effect's cleanup function gets executed (here there is none)
    • 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.