Last active
June 6, 2025 10:24
-
-
Save KurtGokhan/9aafd8e83c9bc6a2946fe2dc7f2c1d19 to your computer and use it in GitHub Desktop.
useCombinedRefs - Old and new
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A combined ref implementation using the callback ref cleanups feature. | |
* This will work in React 19. | |
*/ | |
import { Ref, useCallback } from 'react'; | |
type OptionalRef<T> = Ref<T> | undefined; | |
type Cleanup = (() => void) | undefined | void; | |
function setRef<T>(ref: OptionalRef<T>, value: T): Cleanup { | |
if (typeof ref === 'function') { | |
const cleanup = ref(value); | |
if (typeof cleanup === 'function') { | |
return cleanup; | |
} | |
return () => ref(null); | |
} else if (ref) { | |
ref.current = value; | |
return () => (ref.current = null); | |
} | |
} | |
export function useCombinedRefs<T>(...refs: OptionalRef<T>[]) { | |
// biome-ignore lint/correctness/useExhaustiveDependencies: The hook already lists all dependencies | |
return useCallback((value: T | null) => { | |
const cleanups: Cleanup[] = []; | |
for (const ref of refs) { | |
const cleanup = setRef(ref, value); | |
cleanups.push(cleanup); | |
} | |
return () => { | |
for (const cleanup of cleanups) { | |
cleanup?.(); | |
} | |
}; | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, refs); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A combined ref implementation that will work in React 18 | |
*/ | |
import { ForwardedRef, useCallback, useRef } from 'react'; | |
type OptionalRef<T> = ForwardedRef<T> | undefined; | |
function setRef<T>(ref: OptionalRef<T>, value: T) { | |
if (typeof ref === 'function') { | |
ref(value); | |
} else if (ref) { | |
ref.current = value; | |
} | |
} | |
export function useCombinedRefs<T>(...refs: OptionalRef<T>[]) { | |
const previousRefs = useRef<OptionalRef<T>[]>([]); | |
return useCallback((value: T | null) => { | |
let index = 0; | |
for (; index < refs.length; index++) { | |
const ref = refs[index]; | |
const prev = previousRefs.current[index]; | |
// eslint-disable-next-line eqeqeq | |
if (prev != ref) setRef(prev, null); | |
setRef(ref, value); | |
} | |
for (; index < previousRefs.current.length; index++) { | |
const prev = previousRefs.current[index]; | |
setRef(prev, null); | |
} | |
previousRefs.current = refs; | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, refs); | |
} |
I first assumed the "new" one supported old and new React, but this is not the case. Still useful for app developers but if you're building a library where the React version can be anything a more solid implementation is something like https://github.com/vercel/next.js/blob/canary/packages/next/src/client/use-merged-ref.ts
/*! The MIT License (MIT) Copyright (c) 2025 Vercel, Inc. */
import { useCallback, useRef, type Ref } from 'react'
// This is a compatibility hook to support React 18 and 19 refs.
// In 19, a cleanup function from refs may be returned.
// In 18, returning a cleanup function creates a warning.
// Since we take userspace refs, we don't know ahead of time if a cleanup function will be returned.
// This implements cleanup functions with the old behavior in 18.
// We know refs are always called alternating with `null` and then `T`.
// So a call with `null` means we need to call the previous cleanup functions.
export function useMergedRef<TElement>(
refA: Ref<TElement>,
refB: Ref<TElement>
): Ref<TElement> {
const cleanupA = useRef<(() => void) | null>(null)
const cleanupB = useRef<(() => void) | null>(null)
// NOTE: In theory, we could skip the wrapping if only one of the refs is non-null.
// (this happens often if the user doesn't pass a ref to Link/Form/Image)
// But this can cause us to leak a cleanup-ref into user code (e.g. via `<Link legacyBehavior>`),
// and the user might pass that ref into ref-merging library that doesn't support cleanup refs
// (because it hasn't been updated for React 19)
// which can then cause things to blow up, because a cleanup-returning ref gets called with `null`.
// So in practice, it's safer to be defensive and always wrap the ref, even on React 19.
return useCallback(
(current: TElement | null): void => {
if (current === null) {
const cleanupFnA = cleanupA.current
if (cleanupFnA) {
cleanupA.current = null
cleanupFnA()
}
const cleanupFnB = cleanupB.current
if (cleanupFnB) {
cleanupB.current = null
cleanupFnB()
}
} else {
if (refA) {
cleanupA.current = applyRef(refA, current)
}
if (refB) {
cleanupB.current = applyRef(refB, current)
}
}
},
[refA, refB]
)
}
function applyRef<TElement>(
refA: NonNullable<Ref<TElement>>,
current: TElement
) {
if (typeof refA === 'function') {
const cleanup = refA(current)
if (typeof cleanup === 'function') {
return cleanup
} else {
return () => refA(null)
}
} else {
refA.current = current
return () => {
refA.current = null
}
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The 'use-combined-refs-new.ts' version anticipates the callback ref cleanup feature in React 19, which is yet to be released. The 'use-combined-refs-old.ts' version is tailored for React 18 and employs useRef for maintaining previous refs. Both are useful for combining multiple refs into a single ref.