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.
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}
/>
))}
</>
);
}
You're on the right track, both in terms of thinking and in terms of approach.
It's correct to use an array to store the refs for elements. There are some issues though with your implementation.
When you use the useRef
hook. The generic associated with the function represents the type of ref you want to represent; in react this will usually be Element
, HTMLElement
or some perhaps some other type assignable to one of these. If not dealing with DOM it could be anything but in your case it should be something like this. But in your implementation you are setting the generic as RefObject<HTMLDivElement>
. The implementation of useRef
is likely similar to function useRef<T>(initialValue: T): RefObject<T>;
so the return type is already wrapped in RefObject
. The generic you provide should therefore only be HTMLDivElement
.
Since on first evaluation of the hook the ref could be null since the element is not rendered you, you should check for that before assigning to the ref. You can use callback refs to do this easily so in your ref assignment:
<ChildComponent ref={TODO} value={value} key={value.id} />
You should check the ref is not null, something like ref={el=>el?el:undefined}
if original useRef
accepts undefined. Or something along those lines, depending on how you defined the ref.
When the element is changed, the ref should update if it was implemented correctly.