成果物
今回はクリーンアップ関数を利用してタイマーを作成しました。ただし、正確なタイマーではありません。
動きとしては以下のようになります。
- 「タイマー表示」を押すとタイマーがスタートする
- 各ボタンにより処理が実行される
2.1. リセットボタン:タイマーのカウントを元に戻す
2.2. ストップボタン:タイマーをストップする
2.3. スタートボタン:タイマーを再開する(タイマーが止まっている場合しか使用できない) - 「タイマーを非表示」を押すとタイマーが削除される
クリーンアップ関数
useEffect()
の第1引数に設定された関数から返された関数をクリーンアップ関数といいます。
クリーンアップとはタイマーのキャンセル処理やイベントリスナの削除などで、コンポーネントがレンダリングされる度にイベントが重複してしまうことから、マウント時に実行した処理をアンマウント時に削除する必要があります。
-
マウント
コンポーネントに対応するDOMノード(DOMツリーの1つひとつのオブジェクト)を作成し、既存のDOMツリー(画面に表示するために解釈されたHTML、CSS、JavaScriptによって構築されたDOMツリー)に挿入して最終的なUIに出力するプロセス -
アンマウント
DOMノードが既存のDOMツリーから削除されること -
更新
既存のDOMツリーに存在するDOMノードに変更を加えること
コード
index.js
今回はStricModeをコメントアウトしています。
StricModeに関して詳しくは、https://ja.reactjs.org/docs/strict-mode.html をご覧ください。
簡潔に言うと、StricModeは安全でない副作用を洗い出すために、意図的にコンポーネントを二重レンダリングするそうです。そのため、同じ処理が2回実行されてしまい、予想通りの挙動をしなくなってしまいます。
今回は以下のようにStricModeをコメントアウトすることで対応しました。別の対応方法もあるのですが、本題と外れてしまうため行いません。
// StricModeのインポートをコメントアウト
//import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
/* Appコンポーネントに対して対するStricModeをコメントアウトする */
/*<StrictMode>*/
<App />
/*</StrictMode>*/
);
App.js
// useEffectを利用するためにインポートする
import React, { useState, useEffect } from "react";
import "./styles.css";
const INITIAL_COUNT = 0; // タイマーの初期値
let timer; // setInterval関数を代入するための変数(定数ではないため型はlet)
const Timer = () => {
// タイマーカウントの現在の状態変数count、countを更新するための関数setCount
const [count, setCount] = useState(INITIAL_COUNT);
// スタートボタンの現在の表示を管理する状態変数startFlg
// startFlgを更新するための関数stargFlg
// スタートボタンはタイマーが止まっていなければ表示したくない。
// startFlgがtrueの場合、スタートボタンを表示、falseの場合、スタートボタンを非表示
const [startFlg, setStartFlg] = useState(false);
// タイマーをリセットする関数countResetを定義
const countReset = () => {
setCount(INITIAL_COUNT);
};
// タイマーのカウントを+1する関数countIncrementを定義
const countIncrement = () => {
setCount((prevCount) => prevCount + 1);
console.log("カウントアップ+1");
};
// useEffect()から呼ばれる関数sampleFuncを定義
// 画面の初回レンダリング時に実行される
const sampleFunc = () => {
// 画面上部にダイアログメッセージ「副作用関数が実行されました!」が表示される
alert("副作用関数が実行されました!");
// 関数setIntervalが実行される
// 1000ミリ秒=1秒間隔で関数countIncrementが実行される
// そのため、タイマーのカウントが1秒ごとに+1されていく
timer = setInterval(countIncrement, 1000);
// クリーンアップ関数を定義
return () => {
// 副作用関数が再実行された場合、またはコンポーネントがアンマウントされた際に
// コンソールにメッセージが表示される
// sampleFuncはTimerコンポーネント内の関数であるため、
// Timerコンポーネントがアンマウントされるとクリーンアップ関数が実行される
// Timerコンポーネントはタイマーを非表示ボタンが押されるとアンマウントされる
console.log("timerが削除されました");
};
};
// 第2引数である依存配列が空の配列になっているため、第一引数の関数sampleFuncは
// 画面の初回レンダリング時に実行される
useEffect(sampleFunc, []);
// タイマーを止める関数countStopを定義
const countStop = () => {
// 関数clearIntervalでタイマーを停止する
clearInterval(timer);
// タイマーを止めた時のカウントを保持する
setCount(count);
console.log(count);
// タイマーが止まったため、スタートボタンを表示させるためにsetStartFlgにtrueを設定
setStartFlg(true);
};
// タイマーを再開させる関数countStartを定義
const countStart = () => {
timer = setInterval(countIncrement, 1000);
// タイマーが再び動き始めたため、スタートボタンを非表示にするためにsetStartFlgにfalseを設定
setStartFlg(false);
};
return (
<div className="App">
<p>現在のカウント数:{count}</p>
// 三項演算子を用いてstargFlgにtrueが設定されていれば、スタートボタンを表示するよう設定する
{startFlg ? <button onClick={countStart}>START</button> : ""}
<button onClick={countStop}>STOP</button>
<button onClick={countReset}>RESEST</button>
</div>
);
};
export default function App() {
// Timerコンポーネントの表示有無を管理する状態変数display
// displayを更新する関数setDisplayを設定
const [display, setDisplay] = useState(false);
// Timerコンポーネントの表示有無を管理する状態変数displayの値を変更する関数handleDisplay
const handleDisplay = () => {
setDisplay(!display);
};
return (
<>
<button onClick={handleDisplay}>
{display ? "タイマーを非表示" : "タイマーを表示"}
</button>
// 三項演算子を用いてdisplayにタイマーが設定されていれば、タイマーを表示するよう設定する
{display && <Timer />}
</>
);
}
ポイントは、関数setIntervalを1つの変数timerで管理することで、正常に作用するようにすることです。
styles.css
.App {
font-family: sans-serif;
text-align: center;
}
ライフサイクル
コンポーネントがレンダリングされる度にイベントが重複してしまうなど、クリーンアップが必要であるにも関わらず、クリーンアップ処理ができていない場合、Warningが発生してしまいます。
useEffect()
では第1引数の関数がクリーンアップ関数を返すことにより、マウント時に実行した処理をアンマウント時に解除します。関数が毎回のレンダリング時に実行され、新しい関数を実行する1つ前の関数をクリーンアップします。
このようなマウント処理とアンマウント処理の繰り返し処理のことをライフサイクルといいます。