C
React/上級/Lesson 21

forwardRefとuseImperativeHandle

25分·theory
このチャプター
2/2
TypeScript

forwardRefとuseImperativeHandle

💡 なぜ学ぶべきか?

🎯 ライブラリ開発者が必ず知っておくべき上級テクニックです。
💼 非制御コンポーネントを作成する際に必須です。
大企業の技術面接でよく出題される概念です。
🏢 실무에서는
UIライブラリを作成する際、親コンポーネントが子のinputを直接制御する必要がある場合があります。たとえば「フォーカスを当てる」「値をリセットする」といった命令的な操作が必要なときに、forwardRefとuseImperativeHandleを使用します。これはライブラリ開発者にとって必須のスキルです。

forwardRefとは?

forwardRefとuseImperativeHandle

問題:refはpropsで渡せない

デフォルトでは、refは特殊なpropであるため、子コンポーネントに転送されません。

forwardRef

親から渡されたrefを、子コンポーネント内部のDOMまたはインスタンスに接続します。

useImperativeHandle

forwardRefと組み合わせて使用し、親に公開するインターフェースを明示的に定義します。

ユースケース

  • カスタムInputコンポーネントのfocus()制御
  • スクロール位置の制御
  • フォームライブラリとの統合(React Hook FormのController
  • 再利用可能なコンポーネントライブラリの開発

React 19での変更

React 19以降、forwardRefなしでrefを通常のpropとして受け取ることができます。

💻 forwardRefの実装
import { forwardRef, useRef, useImperativeHandle, useState } from 'react';

// 1. 基本 forwardRef: DOM ref 伝達
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

// forwardRef<ref タイプ, props タイプ>
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div className="flex flex-col gap-1">
        {label && <label className="text-sm font-medium">{label}</label>}
        {/* refを実際の input DOMに接続 */}
        <input
          ref={ref}
          className={`border rounded px-3 py-2 ${
            error ? 'border-red-500' : 'border-gray-300'
          }`}
          {...props}
        />
        {error && <p className="text-red-500 text-xs">{error}</p>}
      </div>
    );
  }
);

CustomInput.displayName = 'CustomInput'; // DevTools 表示名

// 親コンポーネントから ref で input DOM に直接アクセス
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const focusEmail = () => emailRef.current?.focus(); // 外部からフォーカス制御

  return (
    <form>
      <CustomInput ref={emailRef} label="メールアドレス" type="email" />
      <CustomInput ref={passwordRef} label="パスワード" type="password" />
      <button type="button" onClick={focusEmail}>メールアドレスにフォーカス</button>
    </form>
  );
}

// 2. useImperativeHandle: 露出 API カスタマイズ
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (seconds: number) => void;
  getCurrentTime: () => number;
}

interface VideoPlayerProps {
  src: string;
  autoPlay?: boolean;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  ({ src, autoPlay = false }, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);

    // 親に露出するメソッドを定義 (DOM全体ではなく選択されたAPIのみ露出)
    useImperativeHandle(ref, () => ({
      play: () => videoRef.current?.play(),
      pause: () => videoRef.current?.pause(),
      seek: (seconds) => {
        if (videoRef.current) videoRef.current.currentTime = seconds;
      },
      getCurrentTime: () => videoRef.current?.currentTime ?? 0,
    }), []); // 依存関係配列

    return (
      <video
        ref={videoRef}
        src={src}
        autoPlay={autoPlay}
        controls
        className="w-full rounded"
      />
    );
  }
);

// 親: 露出された API のみ使用可能 (DOM全体へのアクセスは不可)
function VideoPage() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <div className="flex gap-2 mt-2">
        <button onClick={() => playerRef.current?.play()}>再生</button>
        <button onClick={() => playerRef.current?.pause()}>停止</button>
        <button onClick={() => playerRef.current?.seek(30)}>30秒に移動</button>
      </div>
    </div>
  );
}

// 3. React 19 方式 (forwardRef 不要)
// function NewInput({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
//   return <input ref={ref} {...props} />;
// }

💡 実践的なヒント

  • displayName: forwardRefコンポーネントにdisplayNameを設定 → React DevToolsで名前が表示される
  • useImperativeHandle推奨: DOM全体を公開する代わりに、必要なメソッドだけを公開してカプセル化を高める
  • React 19: refを通常のpropとして渡せるため、forwardRefは不要(後方互換性は維持)
  • React Hook Form: Controllerコンポーネントの内部でforwardRefを活用
  • TypeScript: forwardRef<RefType, PropsType>ジェネリクスで型安全に使用

⚛️ Reactパターン — forwardRefとuseImperativeHandle

forwardRefとuseImperativeHandleをReactでどのように使うか、コードとともにステップごとに学びましょう。
1 🧩 1. forwardRefとuseImperativeHandleが必要なシナリオ
これらの機能が必要な状況。
2 💻 2. コードの記述
forwardRefとuseImperativeHandleの基本的な使い方。
3 🎨 3. レンダリング結果
ユーザーが見る画面。
4 💡 4. 実務でのヒント
よくある落とし穴とベストプラクティス。

🎮 forwardRefとuseImperativeHandle — ステップごとに理解する

各ステップをクリックして内容を読み、「理解できました」ボタンで進捗を確認しましょう。
🖥️ 実行結果 — レンダリングされたReactコンポーネント
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — まず結果を確認し、エディタで自由に編集できます。

理解度チェック

forwardRefが必要な状況はどれですか?
💡 関数コンポーネントはデフォルトではrefを受け取ることができません。forwardRefでラップすることで、子コンポーネントが親のrefを受け取り、内部のDOM要素に接続できるようになります。UIライブラリのコンポーネントでよく使用されます。
先に読むとよい概念: Error Boundary + Suspense
次のおすすめ: TypeScript + React
forwardRef + useImperativeHandle - React