I have a parent component which uses N
children components, where N
depends on the props in input. For example:
function Parent({ cards }: { cards: Cards[] }) {
return (
<>
{cards.map((value, index) =>
<ChildComponent ref={TODO} value={value} key={value.id} />
}
</>
}
Since I have a bunch of callbacks in the components (not shown) that need to know the location on the screen of each child, I need to create a bunch of refs
in the parent component and pass them to the children, so that later I can use refs[n].current.getBoundingClientRect()
in one of my callbacks.
All the refs need to be recalculated whenever cards
change (cards
is the state of an GrandParent
component, modified as usual with setState(newCards)
).
I already tried many things, but nothing really seems to work.
const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
if (cardsRefs.current.length !== cards.length) {
cardsRefs.current = Array(cards.length)
.fill(null)
.map((_, i) => createRef());
}
...
<ChildComponent ref={cardsRefs.current[index]} value={value} key={value.id} />
This sort of works, but I think it's incorrect - if the cards array changes (keeping the same length), I do need to have new refs, right? Also the use of createRef
sounds weird.
Another try:
const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
...
<ChildComponent
ref={(el) => cardsRef.current[index] = el}
value={value}
key={value.id}
/>
I have no idea how this is meant to work, or why. In any case it does not for me. I get variations of the same error message:
Type 'HTMLDivElement | null' is not assignable to type 'RefObject'. Type 'null' is not assignable to type 'RefObject'
This happens with useRef<React.RefObject<HTMLDivElement>[]>([])
, useRef<HTMLDivElement[]>([])
, useRef([])
- where the error complains about different types every time. Not sure what type this is meant to be.
Another try:
const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
useEffect(() => {
cardsRefs.current = Array(cards.length)
.fill(null)
.map((_, i) => createRef<HTMLDivElement>());
}, [cards]);
This does not work at all, I suspect because useEffect
runs after the DOM has been constructed and therefore the ref
to the child components have already been passed (and are undefined
).
const cardsRefs = useMemo(
() => useRef<React.RefObject<HTMLDivElement>[]>([]),
[cards]
)
This does not work because I cannot call useRef
inside useMemo
.
Ok so basically I don't know what's the right solution here.
Your approaches are close but require minor tweaks.
You are overcomplicating things a bit by trying to use the useEffect
or useMemo
hooks. You can simply compute the refs as needed, using the createRef
utility for the refs you are storing in the cardsRefs
.
In other words, cardsRefs
is a React ref that stores an array of React.RefObject<HTMLDivElement>
refs.
Refs are mutable buckets, so map the cards
array to previously created refs, or create new refs as necessary.
const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
cardsRefs.current = cards.map((_, i) => cardsRefs.current[i] || createRef());
Because you are storing an array of React refs, you not only need to access the current
value of cardsRefs
, but also that of any array element ref.
cardsRefs.current[n].current?.getBoundingClientRect();
^^^^^^^ ^^^^^^^
Parent
function Parent({ cards }: { cards: Cards[] }) {
const cardsRefs = useRef<React.RefObject<HTMLDivElement>[]>([]);
cardsRefs.current = cards.map((_, i) => cardsRefs.current[i] || createRef());
const someCallback = () => {
cardsRefs.current[n].current?.getBoundingClientRect();
};
return (
<>
{cards.map((value, index) => (
<ChildComponent
ref={cardsRefs.current[index]}
value={value}
key={value.id}
/>
))}
</>
);
}
When using the legacy callback syntax the Refs can potentially be null, which is what the error informs you of.
Type 'HTMLDivElement | null' is not assignable to type 'RefObject'. Type 'null' is not assignable to type 'RefObject'
It tells you exactly what the cardsRef
type needs to be, e.g. an array of HTMLDivElement | null
.
const cardsRefs = useRef<(HTMLDivElement | null)[]>([]);
The difference here is that you are not storing an array of React refs, but instead an array of references to an HTML div
element, or null, so the callback access will be a bit different.
cardsRefs.current[n]?.getBoundingClientRect();
^^^^^^^
Parent
function Parent({ cards }: { cards: Cards[] }) {
const cardsRefs = useRef<(HTMLDivElement | null)[]>([]);
const someCallback = () => {
cardsRefs.current[n]?.getBoundingClientRect();
};
return (
<>
{cards.map((value, index) => (
<ChildComponent
ref={(el) => (cardsRefs.current[index] = el)}
value={value}
key={value.id}
/>
))}
</>
);
}