1. ホーム
  2. React

【React】useImperativeHandle の使い方|forwardRef と組み合わせて子の機能を親に公開する

Share

React の ref は、通常だと DOM ノードそのものを指します。ですが「子コンポーネントの中にある input に、親から focus() させたい」「親のボタンを押したら子側のフォームをクリアしたい」といった、親から子の機能を呼び出したい場面があります。そんなときに使うのが useImperativeHandle です。この記事では、useImperativeHandle で子が親に公開する値(メソッド)をカスタマイズする方法を、forwardRef と組み合わせた基本例から実践例まで、初心者〜中級者向けに解説します。あわせて、命令的な操作は最小限にすべき理由も正直にお伝えします。

ref で公開する中身を子が決められる

親が子に ref を渡すと、既定では子のルート DOM ノード(たとえば <input> 要素)が ref.current に入ります。しかしこれだと、親は DOM の生の API(focus()value など)に直接触れることになり、子の内部構造に強く依存してしまいます。

useImperativeHandle を使うと、ref.current に入る値を子コンポーネント側で好きなオブジェクトに差し替えられます。つまり「親に見せたい操作」だけを { focus, clear } のようなメソッドの集まりとして公開し、DOM の実体は隠せるということです。親は公開されたメソッドだけを呼べばよく、子の中身が <input> なのか <textarea> なのかを気にせずに済みます。

forwardRef と組み合わせた基本形

useImperativeHandle は、親から渡された ref を子が受け取れる状態で使います。React 18 までは、子が ref を受け取るために forwardRef でコンポーネントを包む必要があります。基本の形は次のとおりです。

FancyInput.tsx
import { forwardRef, useImperativeHandle, useRef } from "react";

// 親に公開するメソッドの型
export type FancyInputHandle = {
  focus: () => void;
  clear: () => void;
};

// forwardRef で ref を受け取れるようにする
const FancyInput = forwardRef<FancyInputHandle>((props, ref) => {
  // 実際の input を指す内部用の ref
  const inputRef = useRef<HTMLInputElement>(null);

  // ref.current に入る中身を組み立てる
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus();
    },
    clear: () => {
      if (inputRef.current) inputRef.current.value = "";
    },
  }));

  return <input ref={inputRef} type="text" />;
});

export default FancyInput;

ポイントは2つの ref を使い分けていることです。inputRef は子の内部で本物の <input> をつかむための ref、forwardRef の第2引数 ref は親から渡ってきた ref です。useImperativeHandle(ref, () => ({ ... })) の第2引数(関数)が返したオブジェクトが、そのまま親側の ref.current になります。ここでは focusclear の2つだけを公開し、内部の inputRef は外に見せていません。

親側では、公開されたメソッドを持つ ref を作って子に渡し、ボタンなどから呼び出します。

Parent.tsx
import { useRef } from "react";
import FancyInput, { type FancyInputHandle } from "./FancyInput";

export default function Parent() {
  // 公開された { focus, clear } を持つ ref
  const inputRef = useRef<FancyInputHandle>(null);

  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>
        フォーカスする
      </button>
      <button onClick={() => inputRef.current?.clear()}>
        クリアする
      </button>
    </div>
  );
}

親は inputRef.current?.focus() のように、子が公開したメソッドを呼ぶだけです。ref.current は最初のレンダリング時は null なので、?.(オプショナルチェーン)を付けて安全に呼び出しています。子の内部が <input> であることを親は一切知らずに操作できている点に注目してください。

第3引数の依存配列で公開する値を作り直す

useImperativeHandle は第3引数に依存配列を取れます。useMemouseEffect と同じ考え方で、配列に並べた値が変わったときだけ、公開するオブジェクトを作り直します。省略すると毎回のレンダリングで作り直され、空配列 [] にすると最初の一度だけ作られます。

Counter.tsx
import { forwardRef, useImperativeHandle, useState } from "react";

export type CounterHandle = {
  getCount: () => number;
};

const Counter = forwardRef<CounterHandle>((props, ref) => {
  const [count, setCount] = useState(0);

  // count が変わったときだけ公開オブジェクトを作り直す
  useImperativeHandle(
    ref,
    () => ({
      getCount: () => count,
    }),
    [count],
  );

  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
});

export default Counter;

この例で依存配列を [count] にしているのは、公開する getCount がその時点の count を参照するからです。もし依存配列を空 [] にすると、関数は最初の count(0)を閉じ込めたまま更新されず、親が getCount() を呼んでも常に 0 が返ってしまいます。公開するメソッドが state や props を参照する場合は、その値を依存配列に入れておくのが安全です。

入力フォームのバリデーションを親から走らせる

もう少し実践的な例として、入力フィールドを持つ子コンポーネントに「値の検証」と「フォーカス移動」をまとめて公開してみます。親のフォームが送信されたとき、子に検証を走らせ、問題があればエラーを表示してフォーカスを戻す、という流れです。

EmailField.tsx
import { forwardRef, useImperativeHandle, useRef, useState } from "react";

export type EmailFieldHandle = {
  // 検証に成功したら true を返す
  validate: () => boolean;
  focus: () => void;
};

const EmailField = forwardRef<EmailFieldHandle>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [error, setError] = useState("");

  useImperativeHandle(ref, () => ({
    validate: () => {
      const value = inputRef.current?.value ?? "";
      const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
      setError(ok ? "" : "メールアドレスの形式が正しくありません");
      return ok;
    },
    focus: () => inputRef.current?.focus(),
  }));

  return (
    <div>
      <input ref={inputRef} type="email" placeholder="mail@example.com" />
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
});

export default EmailField;

親のフォームは、送信時に子の validate() を呼び、結果を見て後続の処理を分岐します。検証に失敗したら focus() で入力欄にカーソルを戻せます。

SignupForm.tsx
import { useRef, type FormEvent } from "react";
import EmailField, { type EmailFieldHandle } from "./EmailField";

export default function SignupForm() {
  const emailRef = useRef<EmailFieldHandle>(null);

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    const ok = emailRef.current?.validate();
    if (!ok) {
      emailRef.current?.focus(); // 失敗したら入力欄に戻す
      return;
    }
    // 検証に通ったときだけ送信処理へ進む
    console.log("送信します");
  };

  return (
    <form onSubmit={handleSubmit}>
      <EmailField ref={emailRef} />
      <button type="submit">登録</button>
    </form>
  );
}

子が検証ロジックとエラー表示を自分の中に閉じ込め、親には validatefocus という「操作」だけを渡しているのがポイントです。親は入力欄の内部構造を知らなくても、フォーム全体の送信フローを組み立てられます。

使いすぎに注意|まず props と state で解決できないか考える

useImperativeHandle は便利ですが、React では「命令的な操作」はどちらかというと例外的な手段です。React の基本は、状態(state)を変えると画面が宣言的に再描画される、という流れにあります。親から子のメソッドを直接呼ぶやり方は、この流れの外側にあるため、多用すると処理の順番が追いにくくなり、コンポーネント同士の結びつきも強くなります。

そのため、何かを実装する前に「これは props と state で表現できないか」をまず考えるのがおすすめです。たとえば「子に特定のテキストを表示させたい」なら、メソッドを公開するより props で値を渡すほうが自然です。「子を開いた状態にしたい」なら、isOpen のような state を親が持ち、props で子に伝えるほうが React らしい書き方になります。

useImperativeHandle が本当に向いているのは、DOM に近い命令的な操作で、props では素直に表せないものです。具体的には、入力欄へのフォーカス、テキストの選択、要素までのスクロール、メディアの再生・停止、アニメーションの開始などが該当します。公開するメソッドは、こうした「その瞬間に一度だけ実行したい操作」に絞り、内部の state や DOM をまるごと外に出さないようにしましょう。公開する面(インターフェース)が小さいほど、子の作りを後から変えても親に影響しにくくなります。

React 19 では forwardRef なしでも書ける

React 19 からは、ref を通常の props と同じように子コンポーネントの引数で直接受け取れるようになりました。そのため forwardRef で包む必要がなくなり、useImperativeHandle の第1引数には props から受け取った ref をそのまま渡せます。

FancyInput.react19.tsx
import { useImperativeHandle, useRef, type Ref } from "react";

export type FancyInputHandle = {
  focus: () => void;
};

// ref を普通の props として受け取れる(React 19 以降)
function FancyInput({ ref }: { ref: Ref<FancyInputHandle> }) {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
  }));

  return <input ref={inputRef} type="text" />;
}

export default FancyInput;

やっていることは forwardRef 版と同じで、公開するメソッドを useImperativeHandle で組み立てているだけです。まだ React 18 以前のプロジェクトも多いので、本記事では forwardRef を基本にしていますが、React 19 に移行済みなら forwardRef の記述を省けると覚えておくとよいでしょう。

まとめ

useImperativeHandle は、ref.current に入る値を子コンポーネント側でカスタマイズし、親に「公開したい操作」だけをメソッドとして渡すためのフックです。useImperativeHandle(ref, () => ({ focus, clear }), [deps]) の形で、第1引数に受け取った ref、第2引数に公開オブジェクトを返す関数、第3引数にその再生成のタイミングを決める依存配列を指定します。React 18 以前では forwardRef と組み合わせ、内部用の ref で本物の DOM をつかみつつ、外には focusvalidate のような操作だけを見せるのが定石です。ただし命令的な操作は React では例外的な手段なので、まずは props と state で解決できないかを検討し、フォーカスやスクロール、再生・停止といった DOM 寄りの操作に限って、最小限のメソッドを公開するようにしましょう。

参考ページ