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 操作を行う可能性
- 保守性の低下: 子コンポーネントの内部構造を変更しづらくなる
具体的なユースケースとしては、フォームコンポーネントが特定のメソッド(例: focus や reset)だけを親に提供したい場合などがあります。
なぜ制限して渡したいのか?
それは、親コンポーネントが子コンポーネントの内部実装に依存しないようにするためです。
わかりやすくいうと、子コンポーネントの「契約」を明確にするためです。
なぜ明確にする必要があるのか?
それは、将来的に子コンポーネントの実装を変更したり、内部の 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);
}
何が起きているか?
resolveDispatcher()で現在の Dispatcher を取得-
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,
);
}
ポイント解説
-
依存配列に
refを自動追加const effectDeps = deps !== null && deps !== undefined ? deps.concat([ref]) // ← ref を追加 : null;これにより、ref が変わった場合も自動的にハンドルが再生成されます。
-
HookLayoutフラグを使用// packages/react-reconciler/src/ReactHookEffectTags.js export const Layout = /* */ 0b0100;useLayoutEffectと同じタイミング(DOM 更新後、ブラウザ描画前)で実行されます。 -
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, ...); // 違うフラグ
useImperativeHandle は HookLayout フラグを使うため、useLayoutEffect と同じタイミングで実行されます。
なぜこのタイミングなのか?
- DOM 更新後: DOM が存在する状態でハンドルを設定
- 描画前: 親コンポーネントが ref を使う前にハンドルが準備完了
-
同期的:
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ステージ
- mountImperativeHandle: 依存配列に ref を追加し、Layout Effect として登録
-
imperativeHandleEffect:
create()でハンドルを生成し、ref.currentに設定 - updateImperativeHandle: 依存配列が変わった場合のみハンドルを再生成
-
クリーンアップ: アンマウント時に
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 を使う。