こんにちは。
Reactでカウントダウンタイマーアプリを作成し、setIntervalを用いてタイマーの処理を書いていた際、タイマーの停止の処理・タイマーの再開の処理の実装に手こずった箇所がありました。
その中で、この手の実装にはuseRefを使うと良いこと、setIntervalとclearIntervalには「インターバルID」という概念があることを知りました。
これらについて学んだ内容を備忘録代わりに記載したいと思います。
setIntervalと「インターバルID」とは
MDN記載の概要
setInterval() メソッドは、呼び出しによって作成されたインターバルタイマーを一意に識別する正の整数(通常は 1 から 2,147,483,647 の範囲)を返します。
この識別子は、よく「インターバル ID」と呼ばれ、 clearInterval() に渡すことで、指定した関数の反復実行を停止することができます。
同じグローバル環境(特定のウィンドウやワーカーなど)では、元のタイマーがアクティブである限り、インターバル ID は確実に一意となりあり、新しいインターバルタイマーには再利用されません。
引用元:https://developer.mozilla.org/ja/docs/Web/API/Window/setInterval
つまり、setInterval()メソッドを呼び出すたびにそのイベント個別のIDが振られることを意味します。
そのIDをclearInterval()メソッドに渡すとインターバル処理を停止させられます。
MDN記載のコード例
以下はMDN記載の、setInterval()のインターバルIDを格納してインターバル処理を定義し、clearInterval()でインターバル処理を止めているコードの例です。
// intervalID を格納する変数
let intervalId;
function changeColor() {
// 既にインターバルがセットアップされているかどうかを検査
intervalId ??= setInterval(flashText, 1000);
}
function flashText() {
const oElem = document.getElementById("my_box");
oElem.className = oElem.className === "go" ? "stop" : "go";
}
function stopTextColor() {
clearInterval(intervalId);
// 変数から intervalID を解放
intervalId = null;
}
引用元:https://developer.mozilla.org/ja/docs/Web/API/Window/setInterval
ポイントとしては、
-
setInterval()の処理前にインターバルID用の変数を定義している -
setInterval()の処理はインターバルIDがnullかundefinedの時のみ実行されるようにする- インターバルIDがnullかundefinedではない=すでにインターバルIDが設定されている=すでに
setInterval()の処理が走っているということ
- インターバルIDがnullかundefinedではない=すでにインターバルIDが設定されている=すでに
-
setInterval()が実行された段階で、明示的に記載しなくてもintervalIdにインターバルIDの識別子が代入されている -
clearInterval()の処理は事前に定義(代入)されたインターバルIDを基準に実行する -
clearInterval()した時点でクリアしたインターバルIDはもう使わないため、intervalId = null;としてインターバルIDを初期化する
という点です。
ReactでのsetInterval()とuseRef
useRefを使わないケースの問題点
次にReactアプリでsetInterval()を用いるケースとして、まずはインターバルIDを通常の変数として定義した次のコードを考えてみます。
※イベントのトリガーとなるJSXはここでは省略します。
function App() {
const [seconds, setSeconds] = useState<number>(0);
let intervalId;
const startTimer = () => {
if (seconds > 0 && intervalId === null) {
setInterval(
() => setSeconds((seconds) => seconds - 1), 1000
);
}
};
const stopTimer = () => {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
};
}
このコードだと、intervalIdの値が再レンダリングのたびにリセットされてしまうためうまく動きません。
前提:useRefとは
この事象の解決に必要なのがuseRefです。
まずはuseRefとは何なのかを見てみます。
useRef は、レンダー時には不要な値を参照するための React フックです。
ref を変更しても、再レンダーはトリガされないということです。
このことから、 ref は、出力されるコンポーネントの外見に影響しないデータを保存するのに適しています。
例えば、インターバルの ID を保持しておき、あとから利用したい場合、ref に保存することができます。
つまりuseRefを使うことで再レンダリングに左右されずに格納したい値を設定できます。ご丁寧に具体例としてインターバルIDが挙げられています。
useRef は、唯一のプロパティであるcurrentに、指定された初期値が設定された状態の ref オブジェクトを返します。
次回以降のレンダーでも、useRef は同じオブジェクトを返します。このオブジェクトの current プロパティを書き換えることで情報を保存しておき、あとからその値を読み出すことができます。
useRefの値はcurrentプロパティに返されているので、参照したいときはtimerIdRef.currentという形で参照します。
useRefを用いたタイマーの具体例
ここまでで記載したuseRefを用いて、タイマーのコードを書き換えてみます。
function App() {
const [seconds, setSeconds] = useState<number>(0);
+ const timerIdRef = useRef<number | null>(null);
// タイマーを開始する関数
const startTimer = () => {
// 秒数が0以上かつタイマーIDがnull(=未設定)のとき
// タイマーが0になった時に停止する処理は今回の記事では省略
if (seconds > 0 && timerIdRef.current === null) {
timerIdRef.current = setInterval(
() => setSeconds((seconds) => seconds - 1), 1000
);
}
};
// タイマーを停止する関数
const stopTimer = () => {
if (timerIdRef.current !== null) {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
}
};
useRefでタイマーIDを管理し、引数として呼び出すときはtimerIdRef.currentを用いて呼び出すことでタイマーが動くようになりました。
最後に
setInterval()にタイマーIDという概念があること、再レンダリングに左右されず値を設定するためのhooksとしてuseRefがあることを学べました。
今後も有効活用していきたいです。
参考資料