59

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>
  )
}
0

3 Answers 3

87

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"/>

Sign up to request clarification or add additional context in comments.

8 Comments

That's more or less what i needed. Thank you!
If you're using hooks, you should use useRef instead of createRef.
not working if data is dynamic changing(addition, deletion)
@akshayYou can use a map object instead of an array
This is not the proper approach since createRef will create a new ref on every render. Use this approach instead: stackoverflow.com/questions/54633690/…
|
1

There is no need for useEffect with side effects or other brittle approaches. According to the official guidance, you can use "ref callbacks".

React will call your ref callback with the DOM node when it’s time to set the ref, and with null when it’s time to clear it. This lets you maintain your own array or a Map, and access any ref by its index or some kind of ID. e.g:

import { useRef } from "react";

const data = [{
    id: 1,
    text: 'test1'
  },{
    id: 2,
    text: 'test2'
  }
]

const Component = () => {
  const refs = useRef(new Map());

  return (
    <div>
      {data.map((item) => (
        <div
         key={item.id}
         ref={el => el ? refs.current.set(item.id, el) : refs.current.delete(item.id)}>
          {item.text}
        </div>
      ))}
    </div>
  );
};

If your array doesn't have a natural ID, you can always fall back to the good ol' index with similar caveats as using it in other places for a key.

Comments

-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>
}

3 Comments

I thought there might be a way that I don't see... I am pretty new to hooks. I guess I will have to use a class component! Thank you for the answer!
Sorry if it wasn’t clear, you can do it with hooks, just render <Item> inside data.map
this is the best and cleanest solution! the punctuation of the answer is an injustice

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.