skip to Main Content

I know this has been asked many times and the answer is always that the effect does not run when refs are mutated but that is not the issue because I run the effect every second until I have an html element with height. This is because the element is rendered later and has a height of 0 until user fills our something and clicks a button.

Here is the code I’m using:

const [containerHeight, setContainerHeight] = useState(200);
const [checkCompnent, setCheckComponent] = useState(0);

useEffect(() => {
  let clear = setTimeout(() => {
    console.log(
      'in timeout',
      stickyContainerRef.current,
      stickyContainerRef.current?.clientHeight,//allways logs 0
      stickyContainerRef.current?.id
    );
    if (
      stickyContainerRef.current &&
      stickyContainerRef.current.clientHeight
    ) {
      //never happens
      console.log('setting height', stickyContainerRef.current.clientHeight);
      setContainerHeight(stickyContainerRef.current.clientHeight);
    } else {
      console.log('retry');
      setCheckComponent((c) => c + 1);
    }
  }, 1000);
  return () => {
    clearTimeout(clear);
  };
}, [checkCompnent]);


<div id="whynotworky" ref={stickyContainerRef}>

The effect runs every second but the element never has a height other than 0 the id is correctly logged as whynotworky and when I open the dev console and run document.getElementById("whynotworky").clientHeight it’ll give me 2700. It’s almost like React set the ref but then never updates it on later renders. I’m tempted to ditch the broken ref way and just use document.getElementById but I don’t think you’re supposed to do this.

I’m running this in Firefox and React version is 17.0.2

2

Answers


  1. clientHeight is not the height of the browser window. It is the height of the div element. Since it has no text in it (which would cause the div to have at least some height) and the height is not set via CSS, the height must be 0. The behavior is correct.

    From MDN:

    The Element.clientHeight read-only property is zero for elements with no CSS or inline layout boxes; otherwise, it’s the inner height of an element in pixels. It includes padding but excludes borders, margins, and horizontal scrollbars (if present).

    Link to the React Playground I worked on: https://playcode.io/2032664

    Login or Signup to reply.
  2. As you can see, your code works. Click the "Show Content" button, and the numbers would change. However, this polling the DOM in this way is not exact, and may hurt performance by forcing the DOM to re-layout so it can give you the dimensions. In addition, you render the component itself once per second, which hurts performance as well.

    const { useRef, useState, useEffect, Fragment } = React;
    
    const Demo = () => {
      const stickyContainerRef = useRef();
      const [show, setShow] = useState(false);
      const [containerHeight, setContainerHeight] = useState(0);
      const [checkCompnent, setCheckComponent] = useState(0);
    
      useEffect(() => {
        let clear = setTimeout(() => {
          console.log('current height', stickyContainerRef.current.clientHeight);
          if (
            stickyContainerRef.current &&
            stickyContainerRef.current.clientHeight
          ) {
            setContainerHeight(stickyContainerRef.current.clientHeight);
            
            clearTimeout(clear);
          } else {
            console.log('retry');
            setCheckComponent((c) => c + 1);
          }
        }, 1000);
        return () => {
          clearTimeout(clear);
        };
      }, [checkCompnent]);
      
      return (
        <Fragment>
          <div>containerHeight: {containerHeight}</div>
        
          <button onClick={() => setShow(!show) }>Show Content</button>
        
          <div id="whynotworky" ref={stickyContainerRef}>
            {show && <div>content</div>}
          </div>
        </Fragment>
      );
    }
    
    ReactDOM
      .createRoot(root)
      .render(<Demo />);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>

    A better way is to use a ResizeObserver. The observer runs in the background, and will react when any of the observed elements changes in size. You can then take the values from the ResizeObserverEntry objects (preferable way), or check the DOM yourself (may hurt the performance). Click "show content" to see it working.

    const { useRef, useState, useEffect, Fragment } = React;
    
    const Demo = () => {
      const stickyContainerRef = useRef();
      const [clientHeight, setClientHeight] = useState(0);
      const [blockSize, setBlockSize] = useState(0);
      const [show , setShow] = useState(false);
      
      useEffect(() => {
        const resizeObserver = new ResizeObserver((entries, observer) => {
          for (const entry of entries) {
            if(entry.target !== stickyContainerRef.current) continue;
            
            const [contentBoxSize] = entry.contentBoxSize;
            
            setClientHeight(entry.target.clientHeight);
            setBlockSize(contentBoxSize.blockSize);
            
            // optional - stop observing if blockSize is not 0
            if(contentBoxSize.blockSize > 0) observer.unobserve(stickyContainerRef.current);
          }
        });
        
        resizeObserver.observe(stickyContainerRef.current);
        
        return () => {
          resizeObserver.disconnect();
        };
      }, [stickyContainerRef]);
      
      return (
        <Fragment>
          <div>clientHeight: {clientHeight}</div>
          <div>blockSize: {blockSize}</div>
        
          <button onClick={() => setShow(!show) }>Show Content</button>
        
          <div id="whynotworky" ref={stickyContainerRef}>
            {show && <div>content</div>}
          </div>
        </Fragment>
      );
    }
    
    ReactDOM
      .createRoot(root)
      .render(<Demo />);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
    
    <div id="root"></div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search