20

Is it possible to detect when an element reference changes it's height? I tried using the following, but when the height of the element changes for whatever reason, the change is not detected. (Please consider that this must also work in IE11)

useEffect(() => {
  // detect change in reference height
}, [elementRef])
Andrei Rosu
  • 1,169
  • 2
  • 10
  • 26
  • 2
    See: [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) – Yousaf Jun 29 '21 at 09:21

5 Answers5

27

I noticed another answer mentioning to use a ResizeObserver, but the answer is incomplete, so I'd like to add my own.

Using useEffect

You can create a ResizeObserver inside a useEffect-hook, and then do what you want to do when the element changes its size. Remember to disconnect from the resizeObserver in the cleanup function.

useEffect(() => {
  if (!elementRef.current) return;
  const resizeObserver = new ResizeObserver(() => {
    // Do what you want to do when the size of the element changes
  });
  resizeObserver.observe(elementRef.current);
  return () => resizeObserver.disconnect(); // clean up 
}, []);

Using useCallback

As mentioned in the comments, it's also possible to use useCallback instead of useEffect.

You can create a ResizeObserver inside a useCallback-hook, and then do what you want to do when the element changes its size. React has an example on how to measure a DOM node.

const elementRef = useCallback(node => {
  if (!node) return;
  const resizeObserver = new ResizeObserver(() => { 
    // Do what you want to do when the size of the element changes
  });
  resizeObserver.observe(node);
}, []);
John
  • 10,165
  • 5
  • 55
  • 71
  • The ResizeObserver isn't working for me (I am also doing transform: scale() on the div) – John Miller Apr 11 '23 at 15:47
  • I think you need to add ref to the dependency array as well. – Moddah May 05 '23 at 11:28
  • I would be careful about adding the `ref` in the dependency array. See this question: https://stackoverflow.com/questions/60476155/is-it-safe-to-use-ref-current-as-useeffects-dependency-when-ref-points-to-a-dom, for more details. You could use a useCallback instead, and reference the callback to the div-element that you are scaling (see one of the answers in the question I linked to. Maybe it works for your scenario.) – John May 05 '23 at 23:51
  • 1
    React actually [has an example](https://legacy.reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node) for this and they recommend using `useCallback` not `useEffect`. "`wait for the elementRef to be available`" doesn't make sense since that effect won't rerun if the `elementRef` isn't available on first render. – sallf May 31 '23 at 16:12
  • @sallf you're absolutely right. The wording in the comment is wrong, so I will remove that. Using `useCallback` instead of `useEffect` is also better. I will update my answer with an example using `useCallback` as well. – John Jun 01 '23 at 00:09
  • @John close, but I don't think `useCallback` has a cleanup function like `useEffect`. – sallf Jun 01 '23 at 17:29
  • 1
    @sallf totally just read that comment then looked everywhere to see where I said that just to realize that I'm not the only "John" out there – John Miller Jun 02 '23 at 00:18
  • @sallf I think I've been using useCallback the wrong way all this time. Then I'm wondering if useEffect is better for the current example since we need to disconnect from the resize observer when the component unmount? – John Jun 02 '23 at 04:57
4

The most efficient way is to use a resize observer.

You can set it up in a useEffect, and clear it when the component unmounts.

1

You could just use window.

My example below is optimised with a cleanup removeEventListener function should the component unmount.

const BoxAndHeight = () => {
  const ref = React.useRef(null);
  const [height, setHeight] = React.useState(0);

  const onResize = React.useCallback(() => {
    if (ref.current) setHeight(ref.current.clientHeight);
  }, []);

  React.useEffect(() => {
    window.addEventListener("resize", onResize);
    onResize();
    return () => {
      window.removeEventListener("resize", onResize);
    };
  }, []);

  return (
    <div ref={ref} style={style}>
      <h1>My height: {height}</h1>
      <p>
        {dummyContent}
      </p>
    </div>
  );
}

// Render it
ReactDOM.createRoot(
    document.getElementById("root")
).render(<BoxAndHeight />);

const style = {
  border: "2px solid #ccc",
  padding: "10px",
  backgroundColor: "#eee"
};
const dummyContent = "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Distinctio, fugiat. Ex in neque velit perspiciatis recusandae, deleniti illum error ea distinctio obcaecati nisi deserunt ab unde corporis quas magnam quo cupiditate dolor dicta? Eos nostrum delectus suscipit! Hic corrupti, assumenda quo modi rem aperiam voluptas explicabo alias fuga error nulla. Eos tenetur voluptas repellat. Tempore, ab harum. Recusandae impedit adipisci soluta officia sunt quis voluptas, quae ea! Eveniet vero incidunt enim quod, voluptate fugiat maxime deserunt et laudantium quidem, ducimus sunt ratione voluptatem libero neque accusamus praesentium fugit doloremque est nisi excepturi. Quo inventore est soluta culpa quos? Minus, laudantium!";
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
rottitime
  • 1,653
  • 17
  • 29
0

I think you can use elementRef.current.clientHeight in useEffect dependencies in order to listen to tag's height.

I test with this case and it worked.

function App() {
  const tag = useRef();
  const [height, setHeight] = useState(10);
  useEffect(() => {
    console.log("updated", tag?.current?.clientHeight);
  }, [tag?.current?.clientHeight]);

  useEffect(() => {
    setInterval(() => {
      setHeight((height) => height + 10);
      console.log(height, tag.current.clientHeight);
    }, 2000);
  }, []);

  return (
    <div className="App" ref={tag}>
      <div style={{ height: height, backgroundColor: "green" }}></div>
    </div>
  );
}

Here is the codesandbox https://codesandbox.io/embed/reactjs-playground-forked-b8j5n?fontsize=14&hidenavigation=1&theme=dark

do thuan
  • 459
  • 3
  • 11
  • 9
    In your code, the change is detected because you also changed the state of the component. Otherwise, it wouldn't trigger it. – Andrei Rosu Jun 29 '21 at 09:50
-4

Possible solution is to use a React Refs. Try this example as reference:

export default function Home() {
  const divRef = createRef()
  // ...
  return (
    <div style={{ height: '100vh', width: '100vw' }}>
      <div
        ref={divRef} // ...
      ></div>
    </div>
  )
}

Detailed code here.

noszone
  • 190
  • 1
  • 14