Support React 19 ref cleanup functions
Hello!
Not sure if this should be a discussion instead, but with React 19 officially out, I noticed a little incompatibility issue. The mergeRefs function doesnโt handle the new cleanup functions for refs.
Iโve been doing it in a simpler, less elegant way in my projects, and it seems to work ok ๐
import type { Ref } from "react"
function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {
return (value) => {
const cleanups = refs.reduce<VoidFunction[]>((accumulator, ref) => {
if (typeof ref === "function") {
const cleanup = ref(value)
if (typeof cleanup === "function") {
accumulator.push(cleanup)
}
} else if (ref) {
ref.current = value
}
return accumulator
}, [])
return () => {
cleanups.forEach((cleanup) => cleanup())
}
}
}
I thought I should report this just in case anyone else runs into issues with their cleanup functions not running.
Thanks for the awesome work!
Yeah feel free to submit a PR about it! Thank you!
This works perfectly for me, and I also replaced the return type with RefCallback for better type alignment. Thank you!
This works incorrectly because there 2 behaviours for cleanup.
-
From React 18, when a ref has no cleanup function or it is a RefObject then it is called twice: first time with a value and second time with null.
-
from React 19 when a ref has a cleanup function. Then it is called twice differently: first time with a value, and the second time it calls cleanup.
In your case ref.current = value will never be called with null even if a component disappears. It breaks React 18 behaviour.
The correct version is like:
import type { Ref } from "react"
function mergeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {
return (value) => {
const cleanups = refs.reduce<VoidFunction[]>((accumulator, ref) => {
if (typeof ref === "function") {
const cleanup = ref(value)
if (typeof cleanup === "function") {
accumulator.push(cleanup)
} else {
accumulator.push(() => ref(null))
}
} else if (ref) {
ref.current = value
accumulator.push(() => ref.current = null)
}
return accumulator
}, [])
return () => {
cleanups.forEach((cleanup) => cleanup())
}
}
}
But this approach works only in React 19
Yep, you are right! I created the utility for my projects that use React 19, and I didn't consider backward compatibility at all.
We can use your version and avoid the major bump, which I think is nice ๐
Pull request is ready: https://github.com/gregberge/react-merge-refs/pull/38
Done