HextaUI

useComposedRef

自分の要素へのrefを保持しつつ、親から渡されたrefにもそのまま転送します。

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { useComposedRef } from "@/hooks/use-composed-ref"

function CharacterInput({ ref, ...props }: React.ComponentProps<"input">) {
  const [inputRef, setRef] = useComposedRef<HTMLInputElement>(ref)
  const [length, setLength] = React.useState(0)

  return (
    <div className="flex h-9 w-full items-center gap-2 rounded-md bg-muted pe-3 focus-within:ring-3 focus-within:ring-focus-ring">
      <input
        ref={setRef}
        {...props}
        onChange={(event) => {
          props.onChange?.(event)
          setLength(event.target.value.length)
        }}
        className="h-full min-w-0 flex-1 bg-transparent ps-3 text-sm outline-none focus-visible:outline-hidden pointer-coarse:text-touch"
      />
      <button
        type="button"
        hidden={length === 0}
        onClick={() => {
          const input = inputRef.current
          if (input) {
            input.value = ""
            setLength(0)
            input.focus()
          }
        }}
        className="text-xs text-muted-foreground hover:text-foreground"
      >
        Clear
      </button>
    </div>
  )
}

export function UseComposedRefDemo() {
  const ref = React.useRef<HTMLInputElement>(null)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <CharacterInput ref={ref} aria-label="Search" placeholder="Search" />
      <Button variant="outline" size="sm" onClick={() => ref.current?.focus()}>
        Focus from the parent
      </Button>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/use-composed-ref.json

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

import { useComposedRef } from "@/hooks/use-composed-ref"
function SearchInput({ ref, ...props }: React.ComponentProps<"input">) {
  const [inputRef, setRef] = useComposedRef<HTMLInputElement>(ref)

  React.useEffect(() => {
    inputRef.current?.select()
  }, [inputRef])

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

React 19 では、ref は通常の prop です。コンポーネント自身もその要素を必要とする場合、ref は2つあるのに ref 属性は1つだけです。このフックは、読み取り用のオブジェクト ref と、両方の ref を埋めるコールバックを提供します。

function SearchInput({ ref, ...props }: React.ComponentProps<"input">) {
  const inputRef = React.useRef<HTMLInputElement>(null)
  return <input ref={inputRef} {...props} />
}

これはコンパイルされますが、親の ref が失われるため、親では ref.current が null のままです。useComposedRef を使うと、親とコンポーネントの両方が要素を受け取ります。

  • 返される inputRef は通常の RefObject なので、useAutosize や useInvalidShake のように、それを期待するフックに渡せます。
  • 親の ref が変わらない間は setRef も同一性を保つため、React がレンダリングのたびにデタッチと再アタッチを行うことはありません。
  • オブジェクト ref、コールバック ref、ref なしのいずれでも動作します。

転送された ref を自分のオブジェクト ref と1つだけ組み合わせる場合(ほとんどのコンポーネントがそうです)は useComposedRef を使います。3つ以上を組み合わせる場合、そのうちの1つが別のフックのコールバック ref である場合、または転送されたコールバック ref が React 19 のクリーンアップ関数を返す場合は、useMergedRef を使います。useComposedRef は、デタッチ時にクリーンアップを実行する代わりに、コールバック ref を null で呼び出します。

プロパティ型デフォルト
refコンポーネントが受け取った ref。
Ref<T> | undefined–
戻り値説明
[0] RefObject<T | null>要素に対する自分自身の ref。
[1] (node: T | null) => void要素の ref に渡します。両方の ref を埋めます。

Input、Textarea、NativeSelect、Field、InputGroup。