( refA: Ref<TElement>, refB: Ref<TElement> )
| 8 | // We know refs are always called alternating with `null` and then `T`. |
| 9 | // So a call with `null` means we need to call the previous cleanup functions. |
| 10 | export function useMergedRef<TElement>( |
| 11 | refA: Ref<TElement>, |
| 12 | refB: Ref<TElement> |
| 13 | ): Ref<TElement> { |
| 14 | const cleanupA = useRef<(() => void) | null>(null) |
| 15 | const cleanupB = useRef<(() => void) | null>(null) |
| 16 | |
| 17 | // NOTE: In theory, we could skip the wrapping if only one of the refs is non-null. |
| 18 | // (this happens often if the user doesn't pass a ref to Link/Form/Image) |
| 19 | // But this can cause us to leak a cleanup-ref into user code (previously via `<Link legacyBehavior>`), |
| 20 | // and the user might pass that ref into ref-merging library that doesn't support cleanup refs |
| 21 | // (because it hasn't been updated for React 19) |
| 22 | // which can then cause things to blow up, because a cleanup-returning ref gets called with `null`. |
| 23 | // So in practice, it's safer to be defensive and always wrap the ref, even on React 19. |
| 24 | return useCallback( |
| 25 | (current: TElement | null): void => { |
| 26 | if (current === null) { |
| 27 | const cleanupFnA = cleanupA.current |
| 28 | if (cleanupFnA) { |
| 29 | cleanupA.current = null |
| 30 | cleanupFnA() |
| 31 | } |
| 32 | const cleanupFnB = cleanupB.current |
| 33 | if (cleanupFnB) { |
| 34 | cleanupB.current = null |
| 35 | cleanupFnB() |
| 36 | } |
| 37 | } else { |
| 38 | if (refA) { |
| 39 | cleanupA.current = applyRef(refA, current) |
| 40 | } |
| 41 | if (refB) { |
| 42 | cleanupB.current = applyRef(refB, current) |
| 43 | } |
| 44 | } |
| 45 | }, |
| 46 | [refA, refB] |
| 47 | ) |
| 48 | } |
| 49 | |
| 50 | function applyRef<TElement>( |
| 51 | refA: NonNullable<Ref<TElement>>, |
no test coverage detected