Search code examples
javascriptreactjsmaterial-uicallbackref

React DOM element mounts and unmounts on every render when it should instead be updated


According to React docs, callback refs are called when a component mounts (with the value of the DOM element) and unmounts (with null):

React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.

According to the docs on React reconciliation, DOM elements of the same type will be updated instead of replaced when encountered:

When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes.

In the following simple snippet, we log every time the callback ref is called:


import { useState } from "react";
    
export default function App() {
  const [num, setNum] = useState(0);
    
  return (
    <div>
      <table ref={(ref) => console.log(ref)}>
        <tbody>
          <tr>
            <td>{num}</td>
          </tr>
        </tbody>
      </table>
      <button onClick={() => setNum((prev) => prev + 1)}>Click</button>
    </div>
  );
}

... and surprisingly, it is called every time the component renders, indicating that the connected DOM node is also unmounted and mounted every time the component rerenders.

Why is this? Seems I don't understand the docs or there is some other mechanism at play here which I would like to understand.

Code sandbox: https://codesandbox.io/s/table-mount-unmount-lr10wv


Solution

  • It is called because you passed callback as anonymous function, which will be recreated each time you update the state(thus forcing rerender), and when ref gets the new value(recomputed function with new reference) it will trigger callback execution. You can avoid this by stabilizing ref callback reference, like this:

      import { useCallback, useState } from "react";
    
    export default function App() {
      const [num, setNum] = useState(0);
    
      const refCallback = useCallback((ref) => console.log(ref), []);
    
      return (
        <div>
          <table ref={refCallback}>
            <tbody>
              <tr>
                <td>{num}</td>
              </tr>
            </tbody>
          </table>
          <button onClick={() => setNum((prev) => prev + 1)}>Click</button>
        </div>
      );
    }