Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

reactjs

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.

like image 791
Joji Avatar asked Nov 26 '20 05:11

Joji


2 Answers

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.

like image 63
Dimitris Karagiannis Avatar answered Oct 27 '22 00:10

Dimitris Karagiannis


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>
    </>
  );
}
like image 36
Gal Margalit Avatar answered Oct 27 '22 01:10

Gal Margalit