React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、応用解説の「Referencing Values with Refs」をかいつまんでまとめた記事です。ただし、コードにはTypeScriptを加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。
なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。
コンポーネントに何か情報を「記憶」させつつ、その情報による新たなレンダーは引き起こしたくないとき使えるのがref
です。
コンポーネントにref
を加える
コンポーネントにref
を加えるには、useRef
フックがimport
されていなければなりません。フックの呼び出しに与える引数はひとつで、ref
が参照する初期値です。たとえば、つぎのコードでは数値0
を初期値としました。
import { useRef } from 'react';
export default function App() {
const ref = useRef(0);
}
このとき、useRef
が返すのは、つぎのようなオブジェクトです。
{
current: 0 // useRefに渡した初期値
}
現在の値は、ref.current
プロパティから参照してください。このプロパティ値はあえてミュータブルです。つまり、値は読み書きともにできます。Reactが追跡しない、コンポーネントの秘密のポケットのようなものでしょう(Reactの一方向データフローの「非常口」とされる理由です)。
つぎのコードは、ボタンクリックのたびにref.current
の値を加算します(サンプル001)。
import { useRef } from 'react';
export default function Counter() {
const ref = useRef(0);
const handleClick = () => {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
};
return (
<button onClick={handleClick}>Click me!</button>
);
}
サンプル001■React + TypeScript: Referencing Values with Refs 01
このコードで、ref
が参照しているのは数値です。けれど、状態と同じく値とするデータは問いません(文字列やオブジェクト、さらには関数でも参照できます)。状態と異なるのは、ref
が単純なJavaScriptオブジェクトで、current
プロパティの値を取得・変更できることです。
なお、この例では値を増やしても、そのたびにコンポーネントが再レンダリングされないことにご注意ください。状態と同じように、ref
は各再レンダリングの間Reactが保持します。ただし、状態の設定がコンポーネントを再レンダリングするのに対し、ref
の変更にレンダーはともないません。
例: ストップウォッチをつくる
ref
と状態は、ひとつのコンポーネントの中で組み合わせて使えます。たとえば、ストップウォッチをつくってみましょう。ボタンを押して開始と停止ができます。そして、開始ボタンが押されてからの経過時間を示さなければなりません。そのためには、いつ開始ボタンが押されたかと、現在の時刻を把握すべきです。この情報はレンダリングに用いるので、状態にもたせます。
開始ボタンが押されたら、setInterval
で10ミリ秒ごとに時間を更新しましょう(サンプル002)。
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState<number | null>(null);
const [now, setNow] = useState<number | null>(null);
const handleStart = () => {
// カウント開始
setStartTime(Date.now());
setNow(Date.now());
setInterval(() => {
// 10ミリ秒ごとに更新
setNow(Date.now());
}, 10);
};
const secondsPassed =
startTime !== null && now !== null ? (now - startTime) / 1000 : 0;
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>Start</button>
</>
);
}
サンプル002■React + TypeScript: Referencing Values with Refs 02
さらに加えるのは停止ボタンです。押されたときに、今のインターバルは解除して、状態変数now
の更新を止めます。解除するために呼び出すのは、clearInterval
です。ただし、引数には解除するインターバルのIDを渡す必要があります。IDは、開始ボタンを押して呼び出されたsetInterval
の戻り値です。そのため、インターバルIDも保持しておきましょう。IDはレンダリングには用いられません。こういうときに使うのがref
です(サンプル003)。
// import { useState } from 'react';
import { useRef, useState } from 'react';
export default function Stopwatch() {
const intervalRef = useRef<number | null>(null);
const handleStart = () => {
// setInterval(() => {
intervalRef.current = window.setInterval(() => {
setNow(Date.now());
}, 10);
};
const handleStop = () => {
if (!intervalRef.current) return;
clearInterval(intervalRef.current);
};
}
サンプル003■React + TypeScript: Referencing Values with Refs 03
なお、TypeScriptを使うときは、setInterval
の呼び出しにwindow
の参照が必要になります。NodeJSの型@types/node
に依存している場合、window
を省くと戻り値が(number
でなく)NodeJS.Timer
と推論されてしまからです(「TypeScriptでsetInterval()の型が合わない理由と解決方法」参照)。
情報がレンダリングに用いられるときは、状態に保持してください。イベントハンドラが必要とするだけで、変更しても再レンダーの要らない情報には、ref
を使うと効率的なこともあるでしょう。
ref
と状態の違い
ref
は状態と比べて「厳格性」に欠けると感じたかもしれません。ref
を変更するには、状態のような設定関数などまったく使わずに済みます。もっとも、ほとんどの場合には、状態が用いられるでしょう。ref
はあくまで「非常口」で、頻繁に必要とはされません。ref
と状態を比べたのがつぎの表です。
ref |
状態 |
---|---|
useRef(initialValue) はオブジェクト{ current: initialValue } を返します。 |
useState(initialValue) の戻り値は、状態変数の現在値と状態設定関数の配列です([value, setValue] )。 |
ref の値を変えても、レンダーは起こりません。 |
状態の変更は、つねに再レンダーされます。 |
ミュータブル: current プロパティ値の変更と更新は、レンダリングプロセスの外です。 |
イミュータブル: 状態変数は設定関数で変更して、再レンダリングのキューに入れなければなりません。 |
レンダリングの間はcurrent の値を読み書きしないでください。 |
状態の値はいつでも読み込めます。ただし、状態はレンダリングごとのスナップショットなので、個々のレンダリングの値は変わりません。 |
ref
と状態の違いをコードで比べてみましょう。つぎのカウンターは状態を用いた場合です(サンプル004)。count
の値は表示されます。したがって、状態変数を使うのが適しているでしょう。カウンターの値がsetCount()
関数で設定されれば、Reactはコンポーネントを再レンダーし、画面は更新されてカウンターの新たな値に反映されるのです。
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>You clicked {count} times</button>;
}
サンプル004■React + TypeScript: Referencing Values with Refs 04
このカウンターをref
で実装しようとしたのがつぎのコードです(サンプル005)。Reactはref
の値が変わってもコンポーネントを再レンダリングしません。つまり、画面でボタンをクリックしても、カウンターの数値が更新されないのです。けれど、コンソールを見ればconsole.log()
で出力されるcountRef.current
の値は変わっていることが確かめられるでしょう。
export default function Counter() {
// const [count, setCount] = useState(0);
const countRef = useRef(0);
const handleClick = () => {
// setCount(count + 1);
// コンポーネントは再レンダリングされない
countRef.current = countRef.current + 1;
console.log('count':, countRef.current);
};
return (
<button onClick={handleClick}>
{/* You clicked {count} times */}
You clicked {countRef.current} times
</button>
);
}
サンプル005■React + TypeScript: Referencing Values with Refs 05
なお、レンダリング中にref.current
を読み込むと、コードに問題が生じるかもしれません。そのようなときは、状態をお使いください。
useRef
の内部的な動き
useState
とuseRef
は、Reactの内部的に備わるフックです。けれど、原理としてはuseRef
はuseState
で実装することもできます。useRef
フックをコードで示すなら、つぎのとおりです。
const useRef = (initialValue: any) => {
const [ref] = useState({ current: initialValue });
return ref;
};
最初のレンダリングで、useRef
は{ current: initialValue }
を返します。参照はReactに保持されるので、つぎのレンダリング時に返されるオブジェクトもそのまま変わりません。コード例に状態の設定関数がないことにご注目ください。これは、useRef
がつねに同じオブジェクトを返すからです。
ref
はいつ使うのか
通常、ref
を用いるのは、コンポーネントがReactのデータフローから「外に出て」外部APIと通信しなければならないときでしょう。大抵はブラウザAPIで、コンポーネントの外観には影響しない場合です。そのような、数少ない状況をご紹介します。
コンポーネントに保持したい値があって、レンダリングロジックには影響しない場合にref
を選択しましょう。
ref
の適切な使い方
つぎのふたつの原則にしたがうと、コンポーネントは適切に扱えます。
-
ref
は「非常口」です。外部システムやブラウザAPIを扱うのに役立つでしょう。けれど、アプリケーションロジックやデータフローの多くがref
に依存しているなら、設計を考え直すべきかもしれません。 -
レンダリング中に
ref.current
を読み書きしないでください。レンダリングしている間に必要な情報には、代わりに状態を使いましょう。Reactはref.current
がいつ変更されるか知りません。その情報をレンダリングのときに読み取るだけでも、コンポーネントの動きは予測しにくくなります。- ひとつの例外は、はじめのレンダリング時につぎのように一度だけ初期値を与える場合です。
if (!ref.current) ref.current = new Thing()
Reactの状態の制限はref
には適用されません。状態の動きはスナップショットです。同期的には更新されません。それに対して、ref
のcurrent
の値は、変更が直ちに反映されます。
ref.current = 5;
console.log(ref.current); // 5
これは、ref
そのものが標準のJavaScriptオブジェクトだからです。したがって、オブジェクトとして動作します。
また、ミューテーションを避けることは、ref
の扱いでは気にしなくて構いません。変更するオブジェクトがレンダリングに使われないかぎり、Reactはref
やその内容の扱いに関知しないからです。
ref
とDOM
ref
はどのような値でも参照できます。けれど、もっともよく用いられるのは、DOM要素の取得でしょう。たとえば、<input>
要素にプログラムでフォーカスを与えたいときなどです。ref
はJSXで要素のref
属性に<div ref={myRef}>
のように渡します。すると、ReactはそのDOM要素をmyRef.current
に収めるのです。ref
を用いたDOMの操作は記事を改めて解説します。
まとめ
この記事では、つぎのような項目についてご説明しました。
-
ref
はレンダリングには使われない値を保持する「非常口」です。たびたび用いるものではありません。 -
ref
はcurrent
というプロパティをひとつだけもつ単純な標準JavaScriptオブジェクトです。プロパティ値は読み書きできます。 - Reactに対しては、
useRef
フックの呼び出しでref
を取得してください。 - 状態と同じように、
ref
は情報をコンポーネントの再レンダリング間で保持できます。 - 状態と異なり、
ref
のcurrent
値を設定しても、再レンダーされません。 - レンダリング中に
ref.current
を読み書きしないでください。コンポーネントの動きを予測するのが難しくなります。