react-merge-refs icon indicating copy to clipboard operation
react-merge-refs copied to clipboard

Support React 19 ref cleanup functions

Open carloitaben opened this issue 11 months ago โ€ข 2 comments

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!

carloitaben avatar Jan 06 '25 20:01 carloitaben

Yeah feel free to submit a PR about it! Thank you!

gregberge avatar Jan 07 '25 12:01 gregberge

This works perfectly for me, and I also replaced the return type with RefCallback for better type alignment. Thank you!

VdustR avatar Jan 15 '25 02:01 VdustR

This works incorrectly because there 2 behaviours for cleanup.

  1. 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.

  2. 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

mrlika avatar Apr 25 '25 09:04 mrlika

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 ๐Ÿ™‚

carloitaben avatar Apr 25 '25 13:04 carloitaben

Pull request is ready: https://github.com/gregberge/react-merge-refs/pull/38

mrlika avatar Apr 25 '25 14:04 mrlika

Done

gregberge avatar Apr 27 '25 16:04 gregberge