Menu

React useRefの使い方:DOMの参照と保持される値

useRefはコンポーネントに { current } という箱を与えます。中身はレンダリングをまたいで保たれ、変えてもレンダリングは起きません。DOMノードに触れる(フォーカス、スクロール、サイズ計測)ため、またはタイマーのIDや画面に表示しない値を保存するために使います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

useRef は { current: initialValue } というオブジェクトを返し、Reactはコンポーネントが存在する間ずっとそれを保持します。ref.current を変えても、何も再レンダリングされません。そのためrefは2つの仕事に向いています。focus() のようなメソッドを呼べるようにDOMノードを持っておくことと、画面に表示する必要のない値をレンダリング間で覚えておくことです。

refオブジェクトを ref={inputRef} に渡すと、Reactは <input> のDOMノードを inputRef.current に入れます。focus() を select() に変え、何か入力してからもう一度クリックすると、テキストが選択されます。

構文

const myRef = useRef(initialValue);

myRef.current;          // read
myRef.current = value;  // write, no re-render
  • initialValue は最初のレンダリングでだけ使われます。DOMのrefなら null です。
  • Reactはすべてのレンダリングで同じオブジェクトを返します。変わるのは current だけです。
  • current の読み書きは普通のJavaScriptです。Reactはそれを追跡しません。

レンダリングを起こさない値

stateは画面に表示するもののためにあります。refは、コンポーネントが覚えておく必要はあっても表示はしないもののためにあります。次のブロックは、カウンターを1つはrefに、1つはstateに持ち、両方をログに出します。

Ref + 1を何回かクリックしてください。コンソールでは ref.current が増えていきますが、ページは0のままで、レンダリングも記録されません。次にState + 1をクリックすると、コンポーネントがレンダリングされ、画面上のrefのカウンターは、refがすでに持っていた数値に一気に変わります。refはずっと値を保っていました。Reactにはそれを描画する理由がなかっただけです。

refとstateの違い

useStateuseRef
戻り値[value, setValue]{ current: value }
変えたときコンポーネントが再レンダリングされる目に見える変化はない
新しい値が読めるタイミング次のレンダリングですぐに
変え方セッターで代入で、ref.current = x
用途画面に表示するすべてのものタイマーのID、DOMノード、前回の値、フラグ

迷ったら、JSXがその値を読むかどうかを考えてください。読むなら、その値はstateに置くべきです。

タイマーのIDを保存する

setInterval は、後でタイマーを止めるときに必要なIDを返します。これはrefにぴったりです。Stopハンドラはそれを必要としますが、画面には決して表示されず、普通の変数に入れると次のレンダリングで失われてしまうからです。

useRef(null) を let intervalRef = { current: null }; に置き換えて、Startを押し、2秒待ってからStopを押してみてください。タイマーは動き続けます。毎回のtickがコンポーネントをレンダリングし、新しいオブジェクトが作られるので、Stopは何も解除しないのです。

前回の値を覚えておく

エフェクトはレンダリングの後に実行されるので、新しい値をrefに保存されている値と比べ、次回のために新しい値を保存できます。

greenをクリックしてからblueをクリックしてください。コンソールには changed from red to green と changed from green to blue が記録されます。すでに選ばれている色をクリックしても何も記録されません。エフェクトは color が変わったときにだけ実行されるからです。

DOMを扱う

refは、ReactにpropがないブラウザのAPIを呼ぶ手段です。focus()、select()、scrollIntoView()、動画の play()、getBoundingClientRect() でのサイズの読み取りなどです。

const lastItemRef = useRef(null);

function scrollToLast() {
    lastItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}

<li ref={lastItemRef}>Last item</li>

ref.current は、ReactがまだDOMノードを作っていないので最初のレンダリング中は null で、要素が取り除かれた後もまた null になります。ノードが存在するイベントハンドラとエフェクトの中で読んでください。ブラウザが描画する前に要素を測って何かを配置したいなら、useLayoutEffectの中でrefを読みます。

レンダリング中にref.currentを読み書きしない

Reactは、コンポーネント本体が純粋な計算であることを前提にしています。同じpropsとstateなら同じJSXです。refはその仕組みの外で変わるので、それを読み書きするレンダリングは同じ入力に対して違う結果を表示することがあり、Reactは思ったより頻繁に本体を呼ぶことがあります。

function Bad() {
    const count = useRef(0);
    count.current++;              // writing during render
    return <p>{count.current}</p>; // reading during render
}

refの読み書きはイベントハンドラとエフェクトで行ってください。唯一の例外は遅延初期化で、最初のレンダリングで1回だけrefを埋めます。

const playerRef = useRef(null);
if (playerRef.current === null) {
    playerRef.current = new VideoPlayer(); // runs once
}

(上のrefのカウンターのデモは、画面がそれに追従しないことを示すために、あえてJSXの中で clicks.current を読んでいます。実際のコードには真似しないでください。)

refコールバック

refオブジェクトの代わりに、ref に関数を渡すこともできます。Reactは要素が追加されたときに、DOMノードを渡してそれを呼びます。React 19ではその関数がクリーンアップを返すことができ、Reactは要素が取り除かれたときにそれを呼びます。項目ごとに useRef を1回ずつ呼べないリストで便利です。

読み込み時、コンソールには項目ごとに1行ずつ attached が記録されます。Remove lastをクリックすると、detached Cheese だけが表示されます。trackNode はコンポーネントの外で定義されているのでレンダリングのたびに同じ関数であり、Reactはほかの項目に手を出しません。代わりに ref={(node) => { ... }} とインラインで書くと、レンダリングのたびに新しい関数が渡されるので、クリックするたびにすべての項目が切り離されて付け直されます。

自分のコンポーネントにrefを渡す

React 19では、ref は関数コンポーネントにとって普通のpropです。親がrefを作り、子は公開したいDOM要素にそれを付けます。

React 19より前はこれに forwardRef が必要で、今も動きます。forwardRefのページでは、両方の書き方と、useImperativeHandle でいくつかのメソッドだけを公開する方法を紹介しています。

よくある間違い

refの値をJSXで表示する。画面はそれに追従しません。ユーザーに値を見せるべきなら、stateを使ってください。

ref.current をエフェクトの依存値にする。[ref.current] はレンダリング中に値を読み、Reactはそれがいつ変わるかを知らないので、変わってもエフェクトは再実行されません。ノードが付いたことに反応するには、refコールバックを使ってください。

最初のレンダリング中にDOMのrefを読む。Reactがノードを作るまでは null です。エフェクトかハンドラの中で読んでください。

フックを使わずに新しいオブジェクトを作る。コンポーネント本体の const ref = { current: null } は、レンダリングのたびに新しいオブジェクトになります。毎回同じオブジェクトを返してくれるのは useRef だけです。

よくある質問

useRefは何を返しますか?

current というプロパティを1つだけ持ち、渡した初期値がセットされた普通のオブジェクトです。Reactはすべてのレンダリングで同じオブジェクトを返すので、current に入れたものは次回もそこにあります。

useRefとuseStateの違いは何ですか?

stateを変えるとコンポーネントは再レンダリングされますが、ref.current を変えても再レンダリングされません。画面に表示する値にはstateを、タイマーのIDやDOMノードのように、イベントハンドラやエフェクトだけが必要とする値にはrefを使います。

ref.currentがnullになるのはなぜですか?

ReactはDOMノードを作った後、コミットの間に ref.current をそのノードにセットします。最初のレンダリング中はまだ null で、要素が取り除かれた後もまた null になります。イベントハンドラかエフェクトの中で読んでください。

ref.currentを変えると再レンダリングされますか?

いいえ。Reactはそれが変わったことを知りません。新しい値が画面に現れるのは、stateの更新のような別の何かがコンポーネントを再レンダリングしたときだけです。

子コンポーネントにrefを渡すには?

React 19では関数コンポーネントが ref を普通のpropとして受け取るので、<MyInput ref={inputRef} /> と渡し、子がそれを自分の <input> に付けます。古いバージョンでは forwardRef が必要でした。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める