0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Qiita Advent Calender 2025企画】React Hooks を Hackしよう!【Part6: useImperativeHandle完全ガイド】

Posted at

React を使っていると、「子コンポーネントの特定のメソッドだけを親に公開したい」「DOM 全体ではなく、必要な操作だけを外部に見せたい」という場面に出くわすことがあります。そんなとき活躍するのが useImperativeHandle フックです。

1. なぜ useImperativeHandle が必要か

1.1 ref で DOM を公開する問題点

通常、子コンポーネントで ref を受け取って DOM 要素に渡すと、親コンポーネントはその DOM ノード全体にアクセスできてしまいます。

// 子コンポーネント
function MyInput({ ref }) {
  return <input ref={ref} />;
}

// 親コンポーネント
function Parent() {
  const inputRef = useRef(null);
  
  const handleClick = () => {
    inputRef.current.focus();           // ✅ OK
    inputRef.current.style.color = 'red'; // 😱 DOM を直接操作できてしまう
    inputRef.current.remove();            // 😱 要素を削除できてしまう
  };
  
  return <MyInput ref={inputRef} />;
}

これにはいくつかの問題があります:

  • カプセル化の破壊: 子コンポーネントの実装詳細が漏れる
  • 予期しない操作: 親が意図しない DOM 操作を行う可能性
  • 保守性の低下: 子コンポーネントの内部構造を変更しづらくなる

具体的なユースケースとしては、フォームコンポーネントが特定のメソッド(例: focusreset)だけを親に提供したい場合などがあります。

なぜ制限して渡したいのか?
それは、親コンポーネントが子コンポーネントの内部実装に依存しないようにするためです。
わかりやすくいうと、子コンポーネントの「契約」を明確にするためです。

なぜ明確にする必要があるのか?
それは、将来的に子コンポーネントの実装を変更したり、内部の DOM 構造を変えたりしても、親コンポーネントに影響を与えないようにするためです。

1.2 useImperativeHandle による解決

useImperativeHandle を使うと、公開するメソッドを明示的に制限できます。

// 子コンポーネント
function MyInput({ ref }) {
  const inputRef = useRef(null);
  
  // 親に公開するメソッドをカスタマイズ
  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
    // scrollIntoView だけ追加で公開
    scrollIntoView() {
      inputRef.current.scrollIntoView();
    },
  }));
  
  return <input ref={inputRef} />;
}

// 親コンポーネント
function Parent() {
  const inputRef = useRef(null);
  
  const handleClick = () => {
    inputRef.current.focus();           // ✅ OK
    inputRef.current.style.color = 'red'; // ❌ undefined(アクセスできない)
  };
  
  return <MyInput ref={inputRef} />;
}

💡 refとは
ref はコンポーネントや DOM 要素への参照を保持するための仕組みです。useImperativeHandle は ref を通じて親コンポーネントに公開するインターフェースをカスタマイズします。
💡 propsとは
props はコンポーネント間でデータを渡すための仕組みです。useImperativeHandle は props の一種として ref を扱い、親コンポーネントに公開するメソッドを制御します。

*props と ref の違い

  • props: 親から子へデータやコールバックを渡す
  • ref: 子から親へ特定のメソッドやプロパティを公開する
    今回の useImperativeHandle は、ref を通じて親に公開するインターフェースをカスタマイズするためのものです。

1.3 useImperativeHandle の API

useImperativeHandle(ref, createHandle, dependencies?)
引数 説明
ref 親から受け取った ref(props として渡される)
createHandle 公開したいハンドル(メソッドを持つオブジェクト)を返す関数
dependencies 省略可能。依存配列。変更時に createHandle が再実行される

💡 React 19 からの変更
React 19 からは ref は通常の props として渡されます。React 18 以前では forwardRef が必要でした。

2. useImperativeHandle の内部構造を徹底解剖

useImperativeHandle を使うたびに、React の内部では複数のモジュールが連携して動いています。この章では、facebook/react リポジトリの実際のコードを追いながら、その動作原理を解説します。

2.0 全体像: useImperativeHandle が動く仕組み

まず、処理フローを把握しましょう:

🎣 useImperativeHandle(フック呼び出し)
   ↓
⚡ mountEffectImpl / updateEffectImpl(Effect として登録)
   ↓
🏭 imperativeHandleEffect(ハンドル生成・設定)
   ↓
🔗 ref.current = カスタムハンドル

重要なポイント:useImperativeHandle は内部的に useLayoutEffect と同じ仕組みを使っています!

2.1 エントリポイント: packages/react/src/ReactHooks.js

まず、useImperativeHandle を呼ぶとどこに飛ぶのか?

// packages/react/src/ReactHooks.js
export function useImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useImperativeHandle(ref, create, deps);
}

何が起きているか?

  1. resolveDispatcher() で現在の Dispatcher を取得
  2. Dispatcher の useImperativeHandle を呼び出し
    • マウント時: mountImperativeHandle
    • 更新時: updateImperativeHandle

2.2 コア実装: mountImperativeHandle

初回レンダー時の処理

// packages/react-reconciler/src/ReactFiberHooks.js

function mountImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    // create が関数でない場合は警告
    if (typeof create !== 'function') {
      console.error(
        'Expected useImperativeHandle() second argument to be a function ' +
          'that creates a handle. Instead received: %s.',
        create !== null ? typeof create : 'null',
      );
    }
  }

  // 依存配列に ref 自体を追加
  // → ref が変わった場合もハンドルを再生成
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  // フラグの設定
  let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;
  if (
    __DEV__ &&
    (currentlyRenderingFiber.mode & StrictEffectsMode) !== NoMode
  ) {
    fiberFlags |= MountLayoutDevEffect;
  }
  
  // useLayoutEffect と同じ仕組みで Effect を登録
  mountEffectImpl(
    fiberFlags,
    HookLayout,  // ← Layout Effect として登録!
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

ポイント解説

  1. 依存配列に ref を自動追加

    const effectDeps = deps !== null && deps !== undefined 
      ? deps.concat([ref])  // ← ref を追加
      : null;
    

    これにより、ref が変わった場合も自動的にハンドルが再生成されます。

  2. HookLayout フラグを使用

    // packages/react-reconciler/src/ReactHookEffectTags.js
    export const Layout = /*    */ 0b0100;
    

    useLayoutEffect と同じタイミング(DOM 更新後、ブラウザ描画前)で実行されます。

  3. imperativeHandleEffect を Effect として登録
    実際のハンドル設定は imperativeHandleEffect 関数が行います。

2.3 Effect の実行: imperativeHandleEffect

実際にハンドルを ref に設定する処理

// packages/react-reconciler/src/ReactFiberHooks.js

function imperativeHandleEffect<T>(
  create: () => T,
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
): void | (() => void) {
  
  // パターン1: コールバック ref の場合
  if (typeof ref === 'function') {
    const refCallback = ref;
    const inst = create();  // ハンドルを生成
    const refCleanup = refCallback(inst);  // コールバックに渡す
    
    // クリーンアップ関数を返す
    return () => {
      if (typeof refCleanup === 'function') {
        refCleanup();
      } else {
        refCallback(null);  // null を渡してリセット
      }
    };
  } 
  // パターン2: オブジェクト ref の場合
  else if (ref !== null && ref !== undefined) {
    const refObject = ref;
    
    if (__DEV__) {
      // current プロパティがない場合は警告
      if (!refObject.hasOwnProperty('current')) {
        console.error(
          'Expected useImperativeHandle() first argument to either be a ' +
            'ref callback or React.createRef() object. Instead received: %s.',
          'an object with keys {' + Object.keys(refObject).join(', ') + '}',
        );
      }
    }
    
    const inst = create();  // ハンドルを生成
    refObject.current = inst;  // ref.current に設定
    
    // クリーンアップ関数を返す
    return () => {
      refObject.current = null;  // null でリセット
    };
  }
}

2つの ref パターン

パターン 処理
オブジェクト ref useRef() で作成 ref.current = inst で設定
コールバック ref (node) => { ... } ref(inst) で呼び出し
// パターン1: オブジェクト ref
const ref = useRef(null);
<MyInput ref={ref} />
// → ref.current = { focus: ..., scrollIntoView: ... }

// パターン2: コールバック ref
<MyInput ref={(handle) => { console.log(handle); }} />
// → ref({ focus: ..., scrollIntoView: ... }) が呼ばれる

2.4 更新時の処理: updateImperativeHandle

再レンダー時の処理

// packages/react-reconciler/src/ReactFiberHooks.js

function updateImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  if (__DEV__) {
    if (typeof create !== 'function') {
      console.error(
        'Expected useImperativeHandle() second argument to be a function ' +
          'that creates a handle. Instead received: %s.',
        create !== null ? typeof create : 'null',
      );
    }
  }

  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  updateEffectImpl(
    UpdateEffect,
    HookLayout,
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

依存配列による最適化

// updateEffectImpl 内部(簡略化)
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevEffect = hook.memoizedState;
  const prevDeps = prevEffect.deps;
  
  // 依存配列が変わっていなければスキップ
  if (areHookInputsEqual(nextDeps, prevDeps)) {
    hook.memoizedState = pushSimpleEffect(hookFlags, inst, create, nextDeps);
    return;  // Effect を実行しない
  }
  
  // 依存配列が変わった場合のみ Effect を実行
  currentlyRenderingFiber.flags |= fiberFlags;
  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags,
    inst,
    create,
    nextDeps,
  );
}

2.5 実行タイミング: Layout Effect

なぜ Layout Effect なのか?

// useImperativeHandle
mountEffectImpl(fiberFlags, HookLayout, ...);

// useLayoutEffect
mountEffectImpl(fiberFlags, HookLayout, ...);

// useEffect
mountEffectImpl(fiberFlags, HookPassive, ...);  // 違うフラグ

useImperativeHandleHookLayout フラグを使うため、useLayoutEffect と同じタイミングで実行されます。

なぜこのタイミングなのか?

  1. DOM 更新後: DOM が存在する状態でハンドルを設定
  2. 描画前: 親コンポーネントが ref を使う前にハンドルが準備完了
  3. 同期的: useEffect と違い、描画をブロックして確実に設定

2.6 クリーンアップの仕組み

コンポーネントがアンマウントされる時や依存配列が変わった時

function imperativeHandleEffect<T>(create, ref) {
  // ...ハンドルを設定...
  
  // クリーンアップ関数を返す
  return () => {
    if (typeof ref === 'function') {
      ref(null);  // コールバック ref にはnull を渡す
    } else if (ref !== null && ref !== undefined) {
      ref.current = null;  // オブジェクト ref は null でリセット
    }
  };
}

2.7 まとめ: useImperativeHandle の内部構造

処理フローの4ステージ

  1. mountImperativeHandle: 依存配列に ref を追加し、Layout Effect として登録
  2. imperativeHandleEffect: create() でハンドルを生成し、ref.current に設定
  3. updateImperativeHandle: 依存配列が変わった場合のみハンドルを再生成
  4. クリーンアップ: アンマウント時に ref.current = null でリセット

useLayoutEffect との関係

項目 useImperativeHandle useLayoutEffect
Hook フラグ HookLayout HookLayout
実行タイミング DOM 更新後、描画前 DOM 更新後、描画前
目的 ref にハンドルを設定 DOM を同期的に操作

3. 代表的ユースケース

3.1 特定のメソッドのみを公開

interface InputHandle {
  focus: () => void;
  clear: () => void;
}

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

// 使用側
function Form() {
  const inputRef = useRef<InputHandle>(null);
  
  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>フォーカス</button>
      <button onClick={() => inputRef.current?.clear()}>クリア</button>
    </>
  );
}

3.2 複数の DOM 操作をまとめる

interface PostHandle {
  scrollAndFocusAddComment: () => void;
}

function Post({ ref }: { ref: React.Ref<PostHandle> }) {
  const commentsRef = useRef<HTMLDivElement>(null);
  const addCommentRef = useRef<HTMLInputElement>(null);
  
  useImperativeHandle(ref, () => ({
    scrollAndFocusAddComment() {
      commentsRef.current?.scrollIntoView({ behavior: 'smooth' });
      addCommentRef.current?.focus();
    },
  }), []);
  
  return (
    <article>
      <h1>投稿タイトル</h1>
      <div ref={commentsRef}>
        {/* コメント一覧 */}
      </div>
      <input ref={addCommentRef} placeholder="コメントを追加" />
    </article>
  );
}

3.3 状態を含むハンドル

interface CounterHandle {
  getValue: () => number;
  increment: () => void;
  reset: () => void;
}

function Counter({ ref }: { ref: React.Ref<CounterHandle> }) {
  const [count, setCount] = useState(0);
  
  useImperativeHandle(ref, () => ({
    getValue: () => count,
    increment: () => setCount(c => c + 1),
    reset: () => setCount(0),
  }), [count]);  // count が変わるたびにハンドルを更新
  
  return <div>Count: {count}</div>;
}

3.4 動画プレイヤーの制御

interface VideoHandle {
  play: () => void;
  pause: () => void;
  seek: (time: number) => void;
}

function VideoPlayer({ src, ref }: { src: string; ref: React.Ref<VideoHandle> }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  
  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => {
      if (videoRef.current) videoRef.current.currentTime = time;
    },
  }), []);
  
  return <video ref={videoRef} src={src} />;
}

4. パフォーマンスと注意点

4.1 依存配列を正しく設定する

// ❌ 依存配列が空だと、count の値が古いまま
useImperativeHandle(ref, () => ({
  getValue: () => count,  // 常に初期値を返す
}), []);

// ✅ count を依存配列に含める
useImperativeHandle(ref, () => ({
  getValue: () => count,
}), [count]);

4.2 ref の過度な使用を避ける

// ❌ props で表現できるのに ref を使う
function Modal({ ref }) {
  const [isOpen, setIsOpen] = useState(false);
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
  }));
  return isOpen ? <div>Modal</div> : null;
}

// ✅ props で制御する
function Modal({ isOpen, onClose }) {
  return isOpen ? (
    <div>
      Modal
      <button onClick={onClose}>閉じる</button>
    </div>
  ) : null;
}

4.3 命令型 vs 宣言型の使い分け

用途 推奨
フォーカス管理 ref inputRef.current.focus()
スクロール ref element.scrollIntoView()
アニメーション開始 ref player.play()
表示/非表示の切り替え props <Modal isOpen={isOpen} />
データの変更 props/state <List items={items} />

5. トラブルシューティング

5.1 ref.current が null

// ❌ レンダー中に ref を使おうとする
function Parent() {
  const ref = useRef(null);
  console.log(ref.current?.getValue());  // null!
  return <Counter ref={ref} />;
}

// ✅ Effect やイベントハンドラ内で使う
function Parent() {
  const ref = useRef(null);
  
  useEffect(() => {
    console.log(ref.current?.getValue());  // OK
  }, []);
  
  return <Counter ref={ref} />;
}

5.2 TypeScript での型定義

// ハンドルの型を定義
interface MyHandle {
  focus: () => void;
  getValue: () => string;
}

// 子コンポーネント
function MyComponent({ ref }: { ref: React.Ref<MyHandle> }) {
  useImperativeHandle(ref, () => ({
    focus: () => { /* ... */ },
    getValue: () => 'value',
  }));
  return <div />;
}

// 親コンポーネント
function Parent() {
  const ref = useRef<MyHandle>(null);
  
  const handleClick = () => {
    ref.current?.focus();
    const value = ref.current?.getValue();
  };
  
  return <MyComponent ref={ref} />;
}

6. まとめ

この記事で解説した内容は、公式ドキュメントとfacebook/react リポジトリの以下のファイルに基づいています:

useImperativeHandle のエントリポイント

  • packages/react/src/ReactHooks.js
    • useImperativeHandle のエクスポート関数

コア実装

  • packages/react-reconciler/src/ReactFiberHooks.js
    • mountImperativeHandle: 初回レンダー時の処理
    • updateImperativeHandle: 更新時の処理
    • imperativeHandleEffect: ハンドルの生成と設定
    • mountEffectImpl / updateEffectImpl: Effect の登録

Effect フラグ

  • packages/react-reconciler/src/ReactHookEffectTags.js
    • Layout フラグ: useLayoutEffect と同じタイミングで実行


  • useImperativeHandle は親コンポーネントに公開するメソッドをカスタマイズする
  • 内部的には useLayoutEffect と同じ仕組み(HookLayout フラグ)を使用
  • DOM 更新後、ブラウザ描画前のタイミングで同期的にハンドルが設定される
  • 依存配列には自動的に ref が追加される
  • クリーンアップ時に ref.current = null でリセットされる

使い分けの指針: 「props で表現できるか?」— Yes なら props を使い、フォーカス・スクロール・アニメーションなど命令型の操作が必要な場合のみ useImperativeHandle を使う。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?