メインコンテンツまでスキップ

ref の転送:React 19 のやり方

この章で学ぶこと

ref を使って DOM 要素にアクセスする方法を学びます。React 19 では、この書き方が以前よりシンプルになりました。新しいやり方を本筋として学び、古いやり方(forwardRef)も「既存コードを読むため」に押さえます。

ref とは何か

ref(reference)は、DOM 要素への参照を保持する仕組みです。useRef で作り、要素に渡すと、その DOM 要素を直接操作できます。

import { useRef, useEffect } from "react"

function Example() {
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
inputRef.current?.focus() // DOM 要素に直接アクセスしてフォーカス
}, [])

return <input ref={inputRef} />
}

ref が必要な場面

ユースケース説明
フォーカス管理特定の入力欄にフォーカスを当てる
テキスト選択テキストをプログラムで選択する
メディア制御動画や音声の再生・停止
サイズ・位置の取得要素の大きさや座標を測る
サードパーティ連携DOM 操作が必要なライブラリと組み合わせる

通常のスタイリングやイベント処理では ref は不要です。「DOM そのものを触りたい」ときに使います。

React 19:ref は通常の prop

React 19 では、関数コンポーネントが ref を普通の prop として受け取れます

// ✅ React 19:ref を prop として受け取る
function MyButton({ ref, ...props }: React.ComponentProps<"button">) {
return <button ref={ref} {...props} />
}

function App() {
const buttonRef = useRef<HTMLButtonElement>(null)
return <MyButton ref={buttonRef}>Click</MyButton> // そのまま動く
}

{ ref, ...props } のように ref を受け取り、内部の <button> に渡すだけです。特別な仕組みは要りません。

<MyButton ref={buttonRef}>

↓ React 19 では ref は props の一部
function MyButton({ ref, ...props }) {
return <button ref={ref} /> ← そのまま渡せる
}

Button での ref

完成形の章の Button を思い出してください。ref という単語が出てこなかったことに気づきましたか。

function Button({ className, variant, size, asChild = false, ...props }: ...) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props} // ← ref はここに含まれて渡る
/>
)
}

React 19 では ref も普通の prop なので、...props の中に含まれます。<Comp {...props} /> と展開するだけで、ref も一緒に <button>(または Slot)へ渡ります。だから Button のコードには ref を特別扱いする記述が要りません。

具体的に、...props には何が入るのか見てみましょう。

// 使う側
<Button ref={buttonRef} onClick={handleClick} disabled type="submit">
送信
</Button>

このとき、Button 内部の分割代入はこうなります。

// 名前を指定した 4 つは個別に取り出される
// className = undefined / variant = undefined / size = undefined / asChild = false

// それ以外すべてが ...props に残る
props = {
ref: buttonRef, // React 19 では ref も普通の prop
onClick: handleClick,
disabled: true,
type: "submit",
children: "送信", // ← タグの中身も children として入る
}

className / variant / size / asChild は先に抜き取られるので ...props には含まれません(className だけは別途 cn(...) 経由で渡しています)。一方、ref だけでなく children(タグの中身)も ...props に入るのがポイントです。<Comp {...props} /> は、この残り全部をまとめて <button>(または Slot)へ渡しています。

これで、利用者は普通に ref を渡せます。

const buttonRef = useRef<HTMLButtonElement>(null)
<Button ref={buttonRef}>送信</Button>

ref を使った実践例

例1:マウント時に自動フォーカス

import { useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"

function AutoFocusButton() {
const buttonRef = useRef<HTMLButtonElement>(null)

useEffect(() => {
buttonRef.current?.focus()
}, [])

return <Button ref={buttonRef}>自動でフォーカスされます</Button>
}

例2:別のボタンからクリックする

function RemoteClickButton() {
const buttonRef = useRef<HTMLButtonElement>(null)

return (
<>
<Button ref={buttonRef} onClick={() => alert("クリックされました")}>
本体のボタン
</Button>
<button onClick={() => buttonRef.current?.click()}>
上のボタンを遠隔クリック
</button>
</>
)
}

例3:要素のサイズを測る

import { useRef, useEffect, useState } from "react"
import { Button } from "@/components/ui/button"

function MeasureButton() {
const buttonRef = useRef<HTMLButtonElement>(null)
const [size, setSize] = useState({ width: 0, height: 0 })

useEffect(() => {
if (buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect()
setSize({ width: rect.width, height: rect.height })
}
}, [])

return (
<>
<Button ref={buttonRef}>測ってみて</Button>
<p>幅: {size.width}px / 高さ: {size.height}px</p>
</>
)
}

asChild との連携

asChild を使うと、ref は置き換え先の要素(子要素)側に書きます。

<Button asChild>
<a href="/profile" ref={linkRef}>プロフィール</a>
</Button>
// ref は <a> 要素を指す

Button の ref 型は HTMLButtonElement のままなので、asChild で置換した要素の ref は子要素側に書きます。Slot の仕組みは次章で扱います。

補足:以前のやり方「forwardRef」

既存のコードやライブラリでは、forwardRef という書き方をよく見かけます。これは React 18 以前で必要だったやり方です。

当時、関数コンポーネントは ref を prop として受け取れませんでした。そこで forwardRef でラップして ref を受け取っていました。

import { forwardRef } from "react"

// React 18 以前のやり方
const MyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => {
return <button ref={ref} {...props} />
})
MyButton.displayName = "MyButton" // DevTools 表示用

少し前までの shadcn/ui の Button も、この forwardRef で書かれていました。

備考

React 19 で ref を通常の prop として受け取れるようになり、forwardRef は不要になりました。forwardRef は当面そのまま動作しますが、将来 deprecated(非推奨)になり、いずれ削除される予定です(React 19 の時点ではまだ警告は出ません)。React チームは自動で書き換える codemod を提供しているので、古いコードも機械的に移行できます(React 19 公式ブログ)。

つまり、新しく書くコードでは forwardRef は使わず、ref を prop で受け取るのが正解です。forwardRef は「過去のコードを読むときの知識」として覚えておけば十分です。

ref とスタイリングは別の関心事

ref(DOM アクセス)と、cva / cn(スタイリング)は、互いに独立しています。

3 つの独立した関心事
├─ cva … バリアント
├─ cn … 競合解決
└─ ref … DOM アクセス
↓ 組み合わさって 1 つの
<button> または <Slot>

それぞれが別の役割を持ち、組み合わさって 1 つの Button になります。

この章のまとめ

  • ref は DOM 要素への参照(フォーカス・測定・メディア制御などに使う)
  • React 19 では ref を通常の prop として受け取れるforwardRef 不要)
  • Button は ref...props で透過的に渡しているので、特別な記述が要らない
  • forwardRef は以前のやり方。当面動くが将来 deprecated。新規コードでは使わない

次章では、asChild を実現する Slot の仕組みを学びます。