1

I have a dynamic component in which I pass in children as prop. So the props look something like:

interface Props {
   ...some props
   children: React.ReactNode
}

export default Layout({...some props, children}: Props) {...}

I need to access the size of the children elements (height and width), in the Layout component. Note that the children are from completely different components and are non-related.

I can use the Layout component as follow:

<Layout ...some props>
  <Child1 /> // I need to know the height and width of this child
  <Child2 /> // as well as this child
  <Child3 /> // and this child.
</Layout>

How can I do so dynamically? Do I somehow have to convert ReactNode to HTMLDivElement? Note that there is no way I can pass in an array of refs as a prop into Layout. Because that the pages which use Layout are dynamically generated.

Since many doesn't really understand what I meant by dynamically generated. It means that the pages which are using the Layout component can pass in x amount of children. The amount of children is unknown but never 0.

utopia
  • 322
  • 1
  • 4
  • 22
  • You can use ref forwarding, that way you don't have to pass refs as props and can directly access the ref from your custom component. – Kartik Malik Jul 30 '21 at 15:40
  • @KartikMalik I don't really understand. I want to access the heigh and width at the Layout component, not at the Child component. Is there an example of code you can demonstrate further what you meant? – utopia Jul 30 '21 at 15:42
  • Can you show how the page is generated please ? You you are the one choosing how to display the content of your page then it wouldnot be to hard but we need more code to help you. Do you simply add `{children}` to your Layout component ? – Quentin Grisel Jul 30 '21 at 15:46

2 Answers2

2

You can achieve this by using React.Children to dynamically build up a list of references before rendering the children. If you have access to the children element references, you can follow the below approach. If you don't then you can follow the bit at the bottom.

You have access to the children element references

If the children components pass up their element reference, you can use React.Children to loop through each child and get each element reference. Then use this to perform calculations before the children components are rendered.

i.e. This is a very simple example on how to retrieve the references and use them.

interface LayoutWrapperProps {
  onMount: () => void;
}

const LayoutWrapper: React.FC<LayoutWrapperProps> = ({ onMount, children }) => {
  React.useEffect(() => {
    onMount();
  }, [onMount]);

  return <>{children}</>;
};

const Layout: React.FC = ({ children }) => {
  const references = React.useRef<HTMLElement[]>([]);

  React.useEffect(() => {
    references.current = [];
  });

  function getReference(ref: HTMLElement) {
    references.current = references.current.filter(Boolean).concat(ref);
  }

  function getHeights() {
    const heights = references.current.map((ref) =>
      ref?.getBoundingClientRect()
    );
    console.log(heights);
  }

  const clonedChildren = React.Children.map(children, (child) => {
    return React.cloneElement(child as any, {
      ref: getReference
    });
  });

  return <LayoutWrapper onMount={getHeights}>{clonedChildren}</LayoutWrapper>;
};

If you don't have access to the children element references

If the children components aren't passing up an element as the reference, you'll have to wrap the dynamic children components in a component so we can get an element reference. i.e.

const WrappedComponent = React.forwardRef((props, ref) => {
   return (
     <div ref={ref}>
       {props.children}
     </div>
   )
});

When rendering the children components, then the code above that gets the references will work:

<Layout>
  <WrappedComponent>
    <Child1 />
  </WrappedComponent>
</Layout>
ljbc1994
  • 2,044
  • 11
  • 13
  • Thank you. The Approach 1 is what I'm looking for. I'm using typescript, so in this case what type should the array of ref be? – utopia Jul 30 '21 at 15:58
  • The type to use would be `React.RefObject` – ljbc1994 Jul 30 '21 at 16:07
  • cloneElement from ReactNode seems to not work – utopia Aug 05 '21 at 16:06
  • Thank you for follow up. I get some weird error upon trying to do React.cloneElement(...). The error I got is "Type 'undefined' is not assignable to type 'ReactElement ReactElement | null) | (new (props: any) => Component)>'." Example here https://codesandbox.io/s/intelligent-spence-qvsej?file=/src/App.tsx:567-581 – utopia Aug 05 '21 at 17:27
  • 1
    How about this? https://codesandbox.io/s/crazy-fermi-9z5ys?file=/src/App.tsx, this should get the elements on mount as well as subsequent updates – ljbc1994 Aug 05 '21 at 17:50
-1

Since we don't know how your children is built, here is what I can propose you :

import React from 'react';
import { render } from 'react-dom';

const App = () => {
  const el1Ref = React.useRef();
  const el2Ref = React.useRef();

  const [childrenValues, setChildrenValues] = React.useState([]);

  React.useEffect(() => {
    setChildrenValues([
      el1Ref.current.getBoundingClientRect(),
      el2Ref.current.getBoundingClientRect()
    ]);
  }, []);
  return (
    <Parent childrenVals={childrenValues}>
      <span ref={el1Ref}>
        <Child value="Hello" />
      </span>
      <span ref={el2Ref}>
        <Child value="<div>Hello<br />World</div>" />
      </span>
    </Parent>
  );
};

const Parent = ({ children, childrenVals }) => {
  React.useEffect(() => {
    console.log('children values from parent = ', childrenVals);
  });
  return <>{children}</>;
};

const Child = ({ value }) => {
  return <div dangerouslySetInnerHTML={{ __html: value }} />;
};

render(<App />, document.getElementById('root'));

And here is the repro on Stackblitz.

The idea is to manipulate how your children is built.

Quentin Grisel
  • 4,794
  • 1
  • 10
  • 15