I want to dynamically set the heights of some elements using the heights of some other elements. Unfortunately I don't know how to get the references to the "source" objects and always run into an infinite loop. This is what I've tried so far. TimelineData is just a simple object with a few properties to populate the elements:
const TimelineElement : React.FC<{data: TimelineData, ref : any}> = ({data, ref}) => {
return <div className="timeline-element" ref={ref}>
<div className="timeline-element-title">{data.title}</div>
<div className="timeline-element-details">{data.details}</div>
</div>;
}
export const Timeline : React.FC<{data: TimelineData[]}> = ({data}) => {
const refs = data.map(() => createRef<HTMLElement>());
const components = data.map(
(r, i) => <TimelineElement ref={(el : any) => refs[i] = el} data={r}/>
);
const [heights, setHeights] = useState<any[]>([]);
useLayoutEffect(() => {
setHeights(refs.map(
ref => ref.current?.tagName
));
}, [components, refs]);
return <div className="timeline">
{components}
{JSON.stringify(heights)}
</div>;
}
using createRef will create a new ref on each render, that will lead to execute setHeights which will cause re-rednering, and so on, there are few work arounds here, but I'd recommend using useRef hook instead, which will return the same ref
in addition to the above answer, to handle your case, you can store multiple elements in one created ref instead of creating array of refs in a loop:
const refs = useRef([]);
...
...
<TimelineElement ref={(el: any) => (refs.current[i] = el)} data={r} />
...
...
setHeights(refs.current?.map((element) => element.tagName));
Here, you got the issue of refs solved, but you'll have the same infinity loop because of another part, your useLayoutEffect depends on components which will get a new value on each render, here we can use useMemo to solve that:
const components = useMemo(() => data.map(
(r, i) => <TimelineElement ref={(el : any) => refs[i] = el} data={r}/>
), [refs]);
one more thing that needs to be fixed, don't give ref to a functional component, you can rename it to a different name:
<TimelineElement elementRef={(el: any) => (refs.current[i] = el)} data={r} />
so after these points, here is the full code:
const TimelineElement: React.FC<{ data: TimelineData; elementRef: any }> = ({
data,
elementRef
}) => {
return (
<div className="timeline-element" ref={elementRef}>
<div className="timeline-element-title">{data.title}</div>
<div className="timeline-element-details">{data.details}</div>
</div>
);
};
export const Timeline: React.FC<{ data: TimelineData[] }> = ({ data }) => {
const refs = useRef([]);
const [heights, setHeights] = useState<any[]>([]);
const components = useMemo(
() =>
data.map((r, i) => (
<TimelineElement elementRef={(el: any) => (refs.current[i] = el)} data={r} />
)),
[refs]
);
useLayoutEffect(() => {
setHeights(refs.current?.map((element) => element.tagName));
}, [components]);
return (
<div className="timeline">
{components}
{JSON.stringify(heights)}
</div>
);
};
One last note: don't forget to add a unique key to elements in map() ;)