HextaUI

useMergedRef

Combines any number of callback and object refs into one, with React 19 ref cleanup for each of them.

0px
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { useMergedRef } from "@/hooks/use-merged-ref"

function MeasuredBox({
  ref,
  onWidth,
  ...props
}: React.ComponentProps<"div"> & { onWidth: (width: number) => void }) {
  const measureRef = React.useCallback(
    (node: HTMLDivElement | null) => {
      if (!node) {
        return
      }
      const observer = new ResizeObserver(([entry]) =>
        onWidth(Math.round(entry.contentRect.width))
      )
      observer.observe(node)
      return () => observer.disconnect()
    },
    [onWidth]
  )
  const setRef = useMergedRef(ref, measureRef)

  return <div ref={setRef} {...props} />
}

export function UseMergedRefDemo() {
  const ref = React.useRef<HTMLDivElement>(null)
  const [width, setWidth] = React.useState(0)
  const [wide, setWide] = React.useState(false)

  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-4">
      <MeasuredBox
        ref={ref}
        onWidth={setWidth}
        data-wide={wide ? "" : undefined}
        className="flex h-16 w-1/2 items-center justify-center rounded-lg bg-muted font-mono text-sm tabular-nums transition-all duration-300 ease-out-quint data-wide:w-full motion-reduce:transition-none"
      >
        {width}px
      </MeasuredBox>
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => setWide((v) => !v)}>
          Resize
        </Button>
        <Button
          variant="ghost"
          size="sm"
          onClick={() =>
            ref.current?.animate(
              [{ scale: 1 }, { scale: 0.96 }, { scale: 1 }],
              { duration: 240, easing: "ease-out" }
            )
          }
        >
          Nudge via parent ref
        </Button>
      </div>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/use-merged-ref.json

Adds the hook and anything it depends on to your project.

import { useMergedRef } from "@/hooks/use-merged-ref"
function Panel({ ref, ...props }: React.ComponentProps<"div">) {
  const localRef = React.useRef<HTMLDivElement>(null)
  const morphRef = useSizeMorph<HTMLDivElement>({ axis: "height" })
  const setRef = useMergedRef(ref, localRef, morphRef)

  return <div ref={setRef} {...props} />
}

An element has one ref, but components often need to hand it to several places: the parent that forwarded a ref, a local ref for effects, and hooks that work through a callback ref.useMergedRef returns one callback that feeds all of them.

When the element attaches, every ref receives it. Object refs get .current set, and callback refs are called with the node. The merged callback returns a cleanup in the React 19 style. On detach it runs each callback ref's own cleanup, or calls the ref with null if it didn't return one, and resets object refs to null.

const observeRef = React.useCallback((node: HTMLDivElement | null) => {
  if (!node) return
  const observer = new ResizeObserver(onResize)
  observer.observe(node)
  return () => observer.disconnect()
}, [onResize])

const setRef = useMergedRef(ref, observeRef)

That makes callback refs a good home for anything tied to the element's lifetime, like observers and listeners. Setup and teardown live together, and merging them with other refs keeps them working.

useMergedRef(ref, (node) => console.log(node))

const logRef = React.useCallback((node) => console.log(node), [])
useMergedRef(ref, logRef)
  • The merged callback changes whenever any ref changes. An inline arrow is a new ref on every render, so React detaches and reattaches the element each time. Wrap callback refs in useCallback.
  • undefined and null refs are skipped, so you can pass optional props straight in.
  • For one forwarded ref plus a local object ref, useComposedRef is shorter.
PropTypeDefault
...refsObject refs, callback refs or undefined.
Array<Ref<T> | undefined>–
ReturnsDescription
(node: T | null) => () => voidPass to the element's ref. Returns the combined cleanup.

Field and InputGroup.