Home > Software design >  React: Trigger function on child render done
React: Trigger function on child render done

Time:06-21

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

CodePudding user response:

You said,

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.

However, parent useEffect does not do that, It fires only after all the children are mounted and their useEffects are fired.

The value of myref is stored in myref.current So your useEffect should be

 useEffect(() => {
        setMarginBottom(calculateHeight(myref.current));
    });

CodePudding user response:

Why don't you send a function to the child component that is called from the useEffect of the child component.

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

someFunction = () => {
    setMarginBottom(calculateHeight(myref));
}


return (
<div ref={myref}>
    <ResponsiveLine func={someFunction} marginBottom={marginBottom}/>
</div>)
}

// CHILD COMPONENT

const ChildComponent = ({func, marginBotton}) => {
const [marginBottom, setMarginBottom] = useState(60);

useEffect(() => {
    func();
}, []);


return <div></div>

}

CodePudding user response:

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/[email protected]/umd/react.development.js"></script><script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/[email protected]/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>

  • Related