// 子コンポーネントが公開するAPIの型 export type InputFieldHandle = { focus: () => void clear: () => void getValue: () => string } // useImperativeHandle で外から呼べるメソッドを定義する const InputField = forwardRef<InputFieldHandle, Props>((props, ref) => { const [value, setValue] = useState('') const inputRef = useRef<HTMLInputElement>(null) useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), clear: () => { setValue('') }, getValue: () => value, })) return <input ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} /> })
前の記事では forwardRef を使って親から子の DOM 要素に直接アクセスしました。今回はその発展として useImperativeHandle を紹介します。
forwardRef だけでは子の raw な DOM が露出しますが、useImperativeHandle を使うと子コンポーネントが「外から呼べるメソッド」を自分で定義できます。DOMの代わりに focus() / clear() / getValue() といったカスタム API を公開する形です。
構成
3つのコンポーネントで構成します。
InputField.tsx:useImperativeHandleでカスタムAPIを公開する子コンポーネントFocusPanel.tsx:親から受け取ったrefを使って InputField を操作する子コンポーネントApp.tsx:ref を持ち、両コンポーネントをレンダリングする親コンポーネント
InputField.tsx:useImperativeHandle でAPIを定義する
まず公開するAPIの型を定義します。
export type InputFieldHandle = { focus: () => void clear: () => void getValue: () => string }
次に forwardRef と useImperativeHandle を組み合わせます。
import { forwardRef, useImperativeHandle, useRef, useState } from 'react' const InputField = forwardRef<InputFieldHandle, Props>( ({ label, placeholder, onFocus, onKeyDown }, ref) => { const [value, setValue] = useState('') const inputRef = useRef<HTMLInputElement>(null) useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), clear: () => { setValue('') }, getValue: () => value, })) return ( <div className="field"> <label>{label}</label> <input ref={inputRef} value={value} onChange={(e) => setValue(e.target.value)} onFocus={onFocus} onKeyDown={onKeyDown} /> </div> ) } )
ポイントは2つです。
valueの状態はInputField自身がuseStateで管理します。親は state を持ちませんuseImperativeHandleの第2引数に公開するメソッドを返す関数を渡します。外からはref.current.focus()のように呼べます
App.tsx:ref の型が InputFieldHandle になる
親は useRef<InputFieldHandle>(null) で ref を作ります。HTMLInputElement ではなく InputFieldHandle 型になる点が forwardRef だけのときとの違いです。
const nameRef = useRef<InputFieldHandle>(null) const emailRef = useRef<InputFieldHandle>(null)
送信時は getValue() で値を取得し、clear() でリセットします。
const handleSubmit = (e: React.FormEvent) => { e.preventDefault() const name = nameRef.current?.getValue() const email = emailRef.current?.getValue() console.log({ name, email }) emailRef.current?.clear() nameRef.current?.clear() nameRef.current?.focus() }
FocusPanel.tsx:子コンポーネントから InputField を操作する
FocusPanel は App から nameRef / emailRef を props で受け取り、ボタン操作で InputField のメソッドを呼びます。
type Props = { nameRef: RefObject<InputFieldHandle | null> emailRef: RefObject<InputFieldHandle | null> lastFocusedRef: RefObject<InputFieldHandle | null> } function FocusPanel({ nameRef, emailRef, lastFocusedRef }: Props) { return ( <div className="panel"> {/* クリア後に対象フィールドへ移動 */} <button onClick={() => { nameRef.current?.clear(); nameRef.current?.focus() }}> 名前をクリア </button> {/* クリア後に元のフィールドへ戻る */} <button onClick={() => { nameRef.current?.clear(); lastFocusedRef.current?.focus() }}> 名前をクリア(元に戻る) </button> </div> ) }
フォーカス制御の2パターン
クリア後のフォーカス挙動は用途によって使い分けられます。
パターン1:クリア後に対象フィールドへ移動
clear() でリセットした直後に同じフィールドの focus() を呼ぶだけです。「クリアしたらそのフィールドを入力し直す」という流れに向いています。
// FocusPanel.tsx:クリア後に対象フィールドへ移動する nameRef.current?.clear() nameRef.current?.focus()

パターン2:クリア後に元のフィールドへ戻る
直前にフォーカスしていたフィールドを lastFocusedRef で追跡しておき、クリア後に戻します。
// App.tsx:onFocus でフォーカス位置を記録する const lastFocusedRef = useRef<InputFieldHandle | null>(null) <InputField ref={nameRef} onFocus={() => { lastFocusedRef.current = nameRef.current }} ... /> <InputField ref={emailRef} onFocus={() => { lastFocusedRef.current = emailRef.current }} ... />
// FocusPanel.tsx:クリア後に元の位置へ戻す nameRef.current?.clear() lastFocusedRef.current?.focus()

forwardRef との違い
| forwardRef のみ | useImperativeHandle | |
|---|---|---|
| 親が取得するもの | DOM 要素(HTMLInputElement) |
カスタムAPI(InputFieldHandle) |
| 子の state | 親が管理 | 子が自分で管理 |
| 公開する操作 | DOM の全メソッド | 定義したメソッドのみ |
| カプセル化 | 弱い | 強い |
useImperativeHandle は「子の内部実装を隠しながら、必要な操作だけを外に公開したい」場合に適しています。
まとめ
| やりたいこと | 使うもの | ポイント |
|---|---|---|
| 子にカスタムAPIを定義する | useImperativeHandle |
第2引数にメソッドを返す関数を渡す |
| 子が自分で state を管理する | useState を子の中に置く |
親は state を持たない |
| 直前のフォーカス位置を記録する | useRef + onFocus |
クリア後の復元に使う |
| 子から公開するAPIの型を定義する | export type XxxHandle |
親の useRef<XxxHandle> と合わせる |