Last active
December 16, 2024 08:25
-
-
Save KurtGokhan/9aafd8e83c9bc6a2946fe2dc7f2c1d19 to your computer and use it in GitHub Desktop.
useCombinedRefs - Old and new
This file contains 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 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); | |
} |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@alvaro-cuesta I don't know any such implementation. Unfortunately, most implementations don't put so much thought on it.
Personally, I am fine if a ref is called multiple times with the same value or
null
. Because your components must be resilient to multiple renders as well as multiple side effects, for the same reason React callsuseEffect
twice in strict mode.It will be mostly solved when Ref Cleanups are released though. Because then, React will keep store the previous ref instead of us having to keep it.