50

So I have an array of data in and I am generating a list of components with that data. I'd like to have a ref on each generated element to calculate the height. I know how to do it with a Class component, but I would like to do it with React Hooks.

Here is an example explaining what I want to do:

import React, {useState, useCallback} from 'react'
const data = [
  {
    text: 'test1'
  },
  {
    text: 'test2'
  }
]
const Component = () => {
  const [height, setHeight] = useState(0);
  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div>
      {
        data.map((item, index) => 
          <div ref={measuredRef} key={index}>
            {item.text}
          </div>
        )
      }
    </div>
  )
}
Constantin Chirila
  • 1,979
  • 2
  • 19
  • 33

2 Answers2

73

Not sure i fully understand your intent, but i think you want something like this:

const {
  useState,
  useRef,
  createRef,
  useEffect
} = React;

const data = [
  {
    text: "test1"
  },
  {
    text: "test2"
  }
];

const Component = () => {
  const [heights, setHeights] = useState([]);
  const elementsRef = useRef(data.map(() => createRef()));

  useEffect(() => {
    const nextHeights = elementsRef.current.map(
      ref => ref.current.getBoundingClientRect().height
    );
    setHeights(nextHeights);
  }, []);

  return (
    <div>
      {data.map((item, index) => (
        <div ref={elementsRef.current[index]} key={index} className={`item item-${index}`}>
          {`${item.text} - height(${heights[index]})`}
        </div>
      ))}
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Component />, rootElement);
.item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #ccc;
}

.item-0 {
  height: 25px;
}

.item-1 {
  height: 50px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>
Sagiv b.g
  • 30,379
  • 9
  • 68
  • 99
  • 1
    That's more or less what i needed. Thank you! – Constantin Chirila May 06 '19 at 09:31
  • If you're using hooks, you should use useRef instead of createRef. – manelescuer Sep 16 '19 at 12:53
  • 9
    not working if data is dynamic changing(addition, deletion) – akshay Dec 16 '19 at 08:48
  • @akshayYou can use a map object instead of an array – Sagiv b.g Dec 16 '19 at 08:57
  • @giorgim if data is dynamic then put it inside a state – Sagiv b.g Mar 23 '20 at 16:36
  • @Sagivb.g No, what if I have dynamic array of components, for which I am creating those refs, and then at one point I want to add a component, thus create additional ref. In your case *data * is hardcoded. Also createRef is still suspicious as in Hooks docs they don't mention it – Giorgi Moniava Mar 23 '20 at 19:30
  • @giorgim you say use them as a reference to components, but createRef is a way to create this reference. https://reactjs.org/docs/refs-and-the-dom.html#creating-refs – Sagiv b.g Mar 23 '20 at 19:31
  • 2
    This is not the proper approach since `createRef ` will create a new ref on every render. Use this approach instead: https://stackoverflow.com/questions/54633690/how-can-i-use-multiple-refs-for-an-array-of-elements-with-hooks/56063129#56063129 – ultra_rn Jan 28 '22 at 17:52
-1

You have to use a separate set of hooks for each item, and this means you have to define a component for the items (or else you’re using hooks inside a loop, which isn’t allowed).

const Item = ({ text }) => {
    const ref = useRef()
    const [ height, setHeight ] = useState()
    useLayoutEffect(() => {
        setHeight( ref.current.getBoundingClientRect().height )
    }, [])
    return <div ref={ref}>{text}</div>
}
Ben West
  • 4,398
  • 1
  • 16
  • 16