HextaUI

useMergedRef

任意の数のコールバックrefとオブジェクトrefを1つにまとめます。それぞれにReact 19のrefクリーンアップが適用されます。

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

フックと、その依存関係をプロジェクトに追加します。

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} />
}

要素の ref は1つだけですが、コンポーネントは多くの場合、それを複数の場所に渡す必要があります。ref を転送した親、effect 用のローカル ref、コールバック ref を通じて動作するフックなどです。useMergedRef は、それらすべてに渡す1つのコールバックを返します。

要素がアタッチされると、すべての ref がそれを受け取ります。オブジェクト ref は .current が設定され、コールバック ref はノードとともに呼び出されます。マージされたコールバックは React 19 方式のクリーンアップを返します。デタッチ時には、各コールバック ref 自身のクリーンアップを実行し、返していなければ ref を null で呼び出し、オブジェクト ref は 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)

そのため、オブザーバーやリスナーなど、要素の寿命に結び付いたものは、コールバック ref に置くのが適しています。セットアップと破棄が一箇所にまとまり、他の ref とマージしても動作し続けます。

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

const logRef = React.useCallback((node) => console.log(node), [])
useMergedRef(ref, logRef)
  • マージされたコールバックは、いずれかの ref が変わるたびに変わります。インラインのアロー関数はレンダリングのたびに新しい ref になるため、React は毎回要素をデタッチして再アタッチします。コールバック ref は useCallback で包んでください。
  • undefined と null の ref はスキップされるので、省略可能な props をそのまま渡せます。
  • 転送された ref 1つとローカルのオブジェクト ref の組み合わせなら、useComposedRef の方が短く書けます。
プロパティ型デフォルト
...refsオブジェクト ref、コールバック ref、または undefined。
Array<Ref<T> | undefined>–
戻り値説明
(node: T | null) => () => void要素の ref に渡します。統合されたクリーンアップを返します。

Field と InputGroup。