useImperativeHandle

useImperativeHandle で親へ公開する ref の操作を最小限に設計する方法を学ぶ。

useImperativeHandle は、親が ref を通じて呼べる値を子コンポーネントが決める Hook です。DOM ノード全体ではなく、focus のような必要最小限の操作だけを公開したいときに使います。

公開する操作を絞る

React 19 では ref を通常の props として受け取れます。内部の input を直接渡さず、handle を返します。

import { useImperativeHandle, useRef, type Ref } from 'react';

type InputHandle = { focus(): void; clear(): void };

function SearchInput({ ref }: { ref: Ref<InputHandle> }) {
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => { if (inputRef.current) inputRef.current.value = ''; },
  }), []);
  return <input ref={inputRef} type="search" />;
}

親は次のように handle の型だけに依存します。

function SearchPage() {
  const searchRef = useRef<InputHandle>(null);
  return <><SearchInput ref={searchRef} /><button onClick={() => searchRef.current?.focus()}>検索する</button></>;
}

親が DOM の value や classList を触れないため、子の内部構造を変えても影響範囲を小さくできます。

React 18 以前も対象にするライブラリでは、forwardRef で ref を受け取る書き方を使います。

この例で親が知っているのは focus と clear だけです。input が textarea や独自の入力部品へ変わっても、同じ操作を提供し続ければ親を直す必要はありません。これは内部実装を隠すための小さな public API です。

使う判断

通常は props で状態とイベントを渡す方が、データフローを追いやすくなります。フォーカス、スクロール、メディア再生のような命令的なブラウザ操作に限定して使います。親が子の内部 state を広く操作する API は、コンポーネントの境界を壊す兆候です。

落とし穴

handle を生成する関数で参照する reactive な値は依存配列へ含めます。省略すると毎レンダーで handle が作り直されます。公開するメソッドは少なく保ち、DOM 実装を将来変更しても親が壊れない境界にします。

特に clear() が入力値を直接書き換えるなら、入力が controlled か uncontrolled かを確認します。value を state から渡す controlled input では、DOM だけを空にしても次のレンダーで元に戻ります。その場合は親が state を更新する props API の方が正しい設計です。

ref を使わない設計を先に考える

「親のボタンで子を開く」「子の値を親がリセットする」といった要求は、open や onChange を props で渡す controlled component にできることが多いです。命令を命令のまま公開するのではなく、親が持つべき state とイベントへ翻訳できないかを検討します。ref はフォーカスやスクロールのように、宣言的な props へ自然に変換できない操作の最後の選択肢です。

まとめ

useImperativeHandle は ref の公開面を制限する Hook です。props で表現できない操作だけを、小さなメソッドとして公開しましょう。

Sources