ReactでuseImperativeHandleを使ってrefへのアクセスを制限する方法を紹介します。
結論から言うと、useImperativeHandle(ref, () => ({ 公開したいメソッド }))を子コンポーネントで呼ぶと、親のref.currentにはDOM要素そのものではなく、そのオブジェクトだけが入ります。親はfocus()やplay()のような自作メソッドしか呼べなくなり、DOMを直接いじられる事故を防げます。React 18まではforwardRefとセットで使い、React 19以降はrefを通常のpropsとして受け取れるのでforwardRefなしでも書けます。
forwardRefで他のコンポーネントにrefを渡せますが、
DOM操作を通してなんでもできてしまうため、他の開発者に意図していない使われ方をする可能性があります。
そこで使うのが、useImperativeHandleです。
forwardRefに関して知りたい方は、以下を参照してみてください。
他のコンポーネントのDOMを操作できるforwardRefの使い方
useImperativeHandleの使い方
useImperativeHandle(ref, func)という形で子コンポーネントで使用します。
第一引数はref、第二引数は使用したいメソッドを含むオブジェクトを返す関数を指定します。
Input.js
import { forwardRef, useImperativeHandle, useRef } from "react";
const Input = forwardRef((props, ref) => {
// 新しいrefを作成
const inputRef = useRef();
// 渡ってきたrefがアクセスできるメソッドを限定する
useImperativeHandle(ref, () => ({
customFocus() {
inputRef.current.focus();
},
}));
// 新しいrefを使用
return <input type="text" ref={inputRef} />;
});
export default Input;
流れとしては、forwardRef, useImperativeHandle, useRefをインポートします。
そして、新しいrefを作成して、JSXで使用します。
useImperativeHandleの第二引数で渡ってきたrefがアクセスできるメソッドを限定します。
そして親コンポーネントでそのメソッドを実行します。
Sample.js
import { useRef } from "react";
import Input from "./Input";
const Sample = () => {
const ref = useRef();
return (
<>
<Input ref={ref} />
<button onClick={() => ref.current.customFocus()}>
インプット要素にフォーカスを当てる
</button>
</>
);
};
export default Sample;
親コンポーネントで、子コンポーネントで作成したメソッドを実行します。
ref.current.focus()やref.current.value = ''のように、公開していないDOMの操作を書いてもundefinedになり動きません。これが「アクセスを制限する」の意味です。
React 19ではforwardRefなしで書ける
React 19からは、関数コンポーネントがrefを通常のpropsとして受け取れるようになりました。forwardRefで包む必要はなく、useImperativeHandleの使い方自体は変わりません。
import { useImperativeHandle, useRef } from "react";
// React 19以降: refはpropsから受け取る
const Input = ({ ref }) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
customFocus() {
inputRef.current.focus();
},
}));
return <input type="text" ref={inputRef} />;
};
export default Input;
React 18以前のプロジェクトでは引き続きforwardRefが必要です。React 19でもforwardRefは動作しますが、新しく書くコードでは上の形が推奨されています。
依存配列で再生成を抑える
第三引数に依存配列を渡せます。省略すると再レンダリングのたびにオブジェクトを作り直すので、メソッドの中で使うstateやpropsがある場合はそれを依存配列に入れます。
useImperativeHandle(ref, () => ({
customFocus() {
inputRef.current.focus();
},
}), []); // 依存がなければ空配列でよい
ビデオの再生・停止の使用例
import { useState, useRef, forwardRef, useImperativeHandle } from "react";
const Video = forwardRef(({ path }, ref) => {
// 新しいrefを作成
const videoRef = useRef();
// 使えるメソッドを限定
useImperativeHandle(ref, () => ({
customPlay() {
videoRef.current.play();
},
customStop() {
videoRef.current.pause();
},
}));
return (
// 新しいrefを使用
<video className="video" ref={videoRef}>
<source src={path}></source>
</video>
);
});
const Sample = () => {
const [playing, setPlaying] = useState(false);
const ref = useRef();
return (
<div>
<Video ref={ref} path="./video.mp4" />
<button
// 限定したメソッドを使用
onClick={() => {
playing ? ref.current.customStop() : ref.current.customPlay();
setPlaying((prev) => !prev);
}}
>
{playing ? "ストップ" : "再生"}
</button>
</div>
);
};
export default Sample;
流れは全く同じです。
子コンポーネントで新しいrefを作成し、useImperativeHandleで使えるメソッドを限定します。
そして、JSXでref属性に新しいrefを指定して、親コンポーネントではDOMのメソッドではなく、限定した自作のメソッドを使用します。
再生中かどうかの表示は、上のようにuseStateで管理します。useStateの基本はuseStateの使い方と注意点まとめで解説しています。
うまく動かないときの確認
ref.currentがnull: 親の初回レンダリング時点ではまだ子がマウントされていません。クリックなどのイベント内で使うか、useEffectの中で参照します- React 18以前で「Function components cannot be given refs」の警告: 子コンポーネントをforwardRefで包み忘れています
- 公開したメソッドの中の値が古い: 第三引数の依存配列に、そのメソッドが参照するstate・propsを入れます
基本的にはDOMを直接触ることになるので、使用が増えるとどこで何をしているのか混乱する原因となります。そのため、useStateで解決できる場合はuseStateを使ってください。
一部useRefを使う必要のあるケースがあるので、そのときはuseImperativeHandleで実行できるメソッドを限定します。
useRefを使う必要のあるケースについては、以下を参照してみてください。
DOMを直接操作するuseRefの使い方とrefとstateの違い
useRefで「ひとつ前の値」を保持する応用はusePreviousで前の状態と比較する方法、複数コンポーネントで値を共有したい場合はrefではなくuseContextの使いどころを検討してください。