reactjsreact-hooksnivo-react

React: Trigger a function when a child asynchronously updates its DOM after rendering


Within ParentComponent, I render a chart (ResponsiveLine). I have a function (calculateHeight) calculating the height of some DOM elements of the chart.

To work fine, my function calculateHeight have to be triggered once the chart ResponsiveLine is rendered.

Here's my issue: useEffect will trigger before the child is done rendering, so I can't calculate the size of the DOM elements of the chart.

How to trigger my function calculateHeight once the chart ResponsiveLine is done rendering?

Here's a simplified code

const ParentComponent = () => {
    const myref = useRef(null);
    const [marginBottom, setMarginBottom] = useState(60);

    useEffect(() => {
        setMarginBottom(calculateHeight(myref));
    });
    
    
    return (
    <div ref={myref}>
        <ResponsiveLine marginBottom={marginBottom}/>
    </div>)
}

EDIT

I can't edit the child ResponsiveLine, it's from a library


Solution

  • You can use the ResizeObserver API to track changes to the dimensions of the box of the div via its ref (specifically the height, which is the block size dimension for content which is in a language with a horizontal writing system like English). I won't go into the details of how the API works: you can read about it at the MDN link above.

    The ResponsiveLine aspect of your question doesn't seem relevant except that it's a component you don't control and might change its state asynchronously. In the code snippet demonstration below, I've created a Child component that changes its height after 2 seconds to simulate the same idea.

    Code in the TypeScript playground

    <div id="root"></div><script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.18.5/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
    <script type="text/babel" data-type="module" data-presets="tsx,react">
    
    // import ReactDOM from 'react-dom/client';
    // import {useEffect, useRef, useState, type ReactElement} from 'react';
    
    // This Stack Overflow snippet demo uses UMD modules instead of the above import statments
    const {useEffect, useRef, useState} = React;
    
    // You didn't show this function, so I don't know what it does.
    // Here's something in place of it:
    function calculateHeight (element: Element): number {
      return element.getBoundingClientRect().height;
    }
    
    function Child (): ReactElement {
      const [style, setStyle] = useState<React.CSSProperties>({
        border: '1px solid blue',
        height: 50,
      });
    
      useEffect(() => {
        // Change the height of the child element after 2 seconds
        setTimeout(() => setStyle(style => ({...style, height: 150})), 2e3);
      }, []);
    
      return (<div {...{style}}>Child</div>);
    }
    
    function Parent (): ReactElement {
      const ref = useRef<HTMLDivElement>(null);
      const [marginBottom, setMarginBottom] = useState(60);
    
      useEffect(() => {
        if (!ref.current) return;
    
        let lastBlockSize = 0;
    
        const observer = new ResizeObserver(entries => {
          for (const entry of entries) {
            if (!(entry.borderBoxSize && entry.borderBoxSize.length > 0)) continue;
            // @ts-expect-error
            const [{blockSize}] = entry.borderBoxSize;
            if (blockSize === lastBlockSize) continue;
            setMarginBottom(calculateHeight(entry.target));
            lastBlockSize = blockSize;
          }
        });
    
        observer.observe(ref.current, {box: 'border-box'});
        return () => observer.disconnect();
      }, []);
    
      return (
        <div {...{ref}}>
          <div>height: {marginBottom}px</div>
          <Child />
        </div>
      );
    }
    
    const reactRoot = ReactDOM.createRoot(document.getElementById('root')!);
    reactRoot.render(<Parent />);
    
    </script>