Search code examples
javascriptreactjsuse-state

How to measure the height of a child and pass it to a parent in React?


I want to calculate the height of a component and send it to its parent when the page is loaded and resized.

I'm using the below reusable Hook to successfully measure the height of the div inside the header component. But how do I send the height calculated from useDimensions in the child to its parent component as headHeight?

Measuring Hook

import { useState, useCallback, useEffect } from 'react';

function getDimensionObject(node) {
  const rect = node.getBoundingClientRect();

  return {
    width: rect.width,
    height: rect.height,
    top: 'x' in rect ? rect.x : rect.top,
    left: 'y' in rect ? rect.y : rect.left,
    x: 'x' in rect ? rect.x : rect.left,
    y: 'y' in rect ? rect.y : rect.top,
    right: rect.right,
    bottom: rect.bottom
  };
}

export function useDimensions(data = null, liveMeasure = true) {
  const [dimensions, setDimensions] = useState({});
  const [node, setNode] = useState(null);

  const ref = useCallback(node => {
    setNode(node);
  }, []);

  useEffect(() => {
    if (node) {
      const measure = () =>
        window.requestAnimationFrame(() =>
          setDimensions(getDimensionObject(node))
        );
      measure();

      if (liveMeasure) {
        window.addEventListener('resize', measure);
        window.addEventListener('scroll', measure);

        return () => {
          window.removeEventListener('resize', measure);
          window.removeEventListener('scroll', measure);
        };
      }
    }
  }, [node, data]);

  return [ref, dimensions, node];
}

Parent

export default function Main(props: { notifications: Notification[] }) {
    const { notifications } = props;
    const [headHeight, setHeadHeight] = useState(0)

    const updateHeadHeight = () => {
        setHeadHeight(headHeight)
    }

    return (
         <main>
            <Header updateParent={updateHeadHeight}/>
           {headHeight}
        </main>
    )
}

Child

import { useDimensions } from '../../lib/design/measure';
import React, { useState, useLayoutEffect } from 'react';

export default function DefaultHeader(props, data) {
    const [
        ref,
        { height, width, top, left, x, y, right, bottom }
      ] = useDimensions(data);
    ;

    return <>
       <div ref={ref} className="header">
          <h1>Hello!</h1>
       </div>   
    </>
}

Solution

  • Personally, I would call the hook in the Main component and wrap the child component in a forwardRef (check docs here).

    See full example here:

    Main.tsx

    export default function Main(props: { notifications: Notification[] }) {
        const { notifications } = props;
        const [ref, dimensions] = useDimensions()
    
        return (
             <main>
                <Header ref={ref}/>
               {JSON.stringify(dimensions)}
            </main>
        )
    }
    

    What's done here, we just pass the ref down the tree to the child component and we just show the dimensions (testing purposes).

    DefaultHeader.tsx

    import { forwardRef } from "react";
    
    const DefaultHeader = forwardRef((_, ref) => {
      return (
        <>
          <div ref={ref} className="header" >
            <h1>Hello!</h1>
          </div>
        </>
      );
    });
    
    export default DefaultHeader;
    

    Here, we just attach the ref to the container that it has previously been (your example).

    See full example on this CodeSandbox.

    Let me know if you need more explanations on this.