Reactでポモドーロタイマーを作る方法|useEffectとsetIntervalで実装
「Reactでポモドーロタイマーを作りたい」「useEffectやsetIntervalを使ってカウントダウンを実装したい」。
ポモドーロタイマーは、Reactの基本的な機能を練習する題材として使いやすいWebアプリです。
例えば、
- state
- イベント処理
- useEffect
- setInterval
- 条件分岐
- コンポーネント設計
などを組み合わせて作れます。
基本的なポモドーロは、
25分集中 → 5分休憩
というサイクルです。
この記事では、Reactを使ってシンプルなポモドーロタイマーを作る方法を紹介します。
Reactで作るポモドーロタイマーの機能
まずは最低限の機能を決めます。
今回作るのは、
- 25分の集中タイマー
- 5分の休憩タイマー
- 開始
- 一時停止
- リセット
- 集中と休憩の自動切り替え
に対応したシンプルなタイマーです。
最初からタスク管理や統計などを追加せず、まずカウントダウン部分を完成させます。
Reactプロジェクトを用意する
すでにReactプロジェクトがある場合は、そのまま利用できます。
新しく作る場合は、現在利用しているReactの開発環境に合わせてプロジェクトを作成してください。
タイマー部分は通常のReactコンポーネントとして実装できます。
例えば、
function PomodoroTimer() {
return (
<main>
<h1>Pomodoro Timer</h1>
</main>
);
}
export default PomodoroTimer;
という状態から始めます。
残り時間をstateで管理する
まず必要なのが残り時間です。
ReactではuseStateを使って管理できます。
25分を秒へ変換すると、
25 × 60 = 1500秒
です。
そのため、
import { useState } from "react";
const FOCUS_TIME = 25 * 60;
function PomodoroTimer() {
const [timeLeft, setTimeLeft] = useState(FOCUS_TIME);
return (
<main>
<h1>Pomodoro Timer</h1>
<p>{timeLeft}</p>
</main>
);
}
export default PomodoroTimer;
とします。
この段階では、
1500
と表示されます。
分と秒に変換する
タイマーとして表示するなら、
24:59
のような形式にします。
分は、
Math.floor(timeLeft / 60)
秒は、
timeLeft % 60
で求められます。
例えば、
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
とします。
秒を常に2桁にするには、
String(seconds).padStart(2, "0")
を使えます。
表示部分は、
<p>
{minutes}:{String(seconds).padStart(2, "0")}
</p>
とします。
これで、
25:00
と表示できます。
タイマーが動いているかstateで管理する
次に、
タイマー実行中か
を管理します。
const [isRunning, setIsRunning] = useState(false);
を追加します。
開始ボタンでは、
<button onClick={() => setIsRunning(true)}>
開始
</button>
停止ボタンでは、
<button onClick={() => setIsRunning(false)}>
停止
</button>
とします。
ただし、これだけでは残り時間は減りません。
useEffectとsetIntervalで1秒ずつ減らす
タイマーを動かすためにuseEffectを使います。
基本形は、
useEffect(() => {
const intervalId = setInterval(() => {
// 処理
}, 1000);
return () => clearInterval(intervalId);
}, []);
です。
ポモドーロタイマーでは、isRunningがtrueの場合だけタイマーを動かします。
useEffect(() => {
if (!isRunning) {
return;
}
const intervalId = setInterval(() => {
setTimeLeft((prev) => prev - 1);
}, 1000);
return () => clearInterval(intervalId);
}, [isRunning]);
これで、開始すると1秒ずつ残り時間が減ります。
clearIntervalが重要
setIntervalを使う場合は、EffectのcleanupでclearIntervalすることが重要です。
return () => clearInterval(intervalId);
cleanupを入れないままEffectが再実行されると、複数のintervalが動いてしまう可能性があります。
その結果、
1秒ごとではなく2秒ずつ減る
など、意図しない動作につながります。
タイマーではcleanupを正しく実装しておきましょう。
0秒より下にならないようにする
今のままだと、
00:00
のあとも、
-1
-2
-3
と減ってしまいます。
そこで、残り時間を更新するときに0を確認します。
setTimeLeft((prev) => {
if (prev <= 1) {
return 0;
}
return prev - 1;
});
さらに0になったら、集中と休憩を切り替える処理を追加します。
集中と休憩のモードを管理する
次に、
集中
と
休憩
を切り替えられるようにします。
const [mode, setMode] = useState("focus");
とします。
時間も定数化しておきます。
const FOCUS_TIME = 25 * 60;
const BREAK_TIME = 5 * 60;
集中終了時には、
setMode("break");
setTimeLeft(BREAK_TIME);
休憩終了時には、
setMode("focus");
setTimeLeft(FOCUS_TIME);
と切り替えます。
タイマー終了時にモードを切り替える
Effectを次のようにします。
useEffect(() => {
if (!isRunning) {
return;
}
const intervalId = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
if (mode === "focus") {
setMode("break");
return BREAK_TIME;
}
setMode("focus");
return FOCUS_TIME;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(intervalId);
}, [isRunning, mode]);
これで、
25分集中
↓
5分休憩
↓
25分集中
と切り替えられます。
シンプルな完成例
ここまでをまとめると、次のような形になります。
import { useEffect, useState } from "react";
const FOCUS_TIME = 25 * 60;
const BREAK_TIME = 5 * 60;
function PomodoroTimer() {
const [mode, setMode] = useState("focus");
const [timeLeft, setTimeLeft] = useState(FOCUS_TIME);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) {
return;
}
const intervalId = setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
if (mode === "focus") {
setMode("break");
return BREAK_TIME;
}
setMode("focus");
return FOCUS_TIME;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(intervalId);
}, [isRunning, mode]);
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
const handleReset = () => {
setIsRunning(false);
setMode("focus");
setTimeLeft(FOCUS_TIME);
};
return (
<main>
<h1>Pomodoro Timer</h1>
<p>{mode === "focus" ? "集中" : "休憩"}</p>
<p>
{minutes}:{String(seconds).padStart(2, "0")}
</p>
<button onClick={() => setIsRunning(true)}>
開始
</button>
<button onClick={() => setIsRunning(false)}>
停止
</button>
<button onClick={handleReset}>
リセット
</button>
</main>
);
}
export default PomodoroTimer;
これだけでも基本的なポモドーロタイマーとして動かせます。
一時停止と再開
一時停止は、
setIsRunning(false);
です。
再開すると、
setIsRunning(true);
になります。
timeLeftはそのままstateに残っているため、
残り12:35で停止
↓
再開
すると、
12:35から続き
をカウントできます。
リセットする
リセットでは、
- 停止
- 集中モード
- 25分
へ戻します。
const handleReset = () => {
setIsRunning(false);
setMode("focus");
setTimeLeft(FOCUS_TIME);
};
これで初期状態へ戻せます。
スキップ機能を追加する
ポモドーロタイマーでは、
今の集中時間を飛ばして休憩へ行く
などのスキップ機能も便利です。
例えば、
const handleSkip = () => {
if (mode === "focus") {
setMode("break");
setTimeLeft(BREAK_TIME);
} else {
setMode("focus");
setTimeLeft(FOCUS_TIME);
}
};
とします。
ボタンは、
<button onClick={handleSkip}>
スキップ
</button>
です。
長い休憩を追加する
一般的なポモドーロでは、集中を何セットか繰り返したあとに長い休憩を取ります。
例えば、
4セット終了
↓
15分休憩
という形です。
その場合は、
const LONG_BREAK_TIME = 15 * 60;
を追加します。
さらに、
const [completedPomodoros, setCompletedPomodoros] = useState(0);
のように、完了した集中セット数を管理します。
4回目の集中終了後だけ長い休憩へ切り替える条件を追加できます。
モードを3種類にする
長い休憩を追加するなら、
focus
shortBreak
longBreak
の3種類にすると管理しやすくなります。
例えば、
const [mode, setMode] = useState("focus");
を使い、
if (mode === "focus") {
// 集中
}
if (mode === "shortBreak") {
// 短い休憩
}
if (mode === "longBreak") {
// 長い休憩
}
という形にします。
規模が大きくなってきたら、モードごとの設定をオブジェクトへまとめてもよいでしょう。
時間設定を変更できるようにする
実際のポモドーロタイマーでは、
25分固定
ではなく時間を変更できると便利です。
例えば、
const [focusMinutes, setFocusMinutes] = useState(25);
const [breakMinutes, setBreakMinutes] = useState(5);
として、入力フォームから変更できるようにします。
これなら、
15分集中 → 5分休憩
45分集中 → 10分休憩
などにも対応できます。
setIntervalだけに時間を任せすぎない
実際のタイマーを作るときに注意したいのが、setIntervalの精度です。
ブラウザのタブがバックグラウンドになったり、PCがスリープしたりすると、コールバックが必ず正確に1秒ごと実行されるとは限りません。
そのため、本格的なタイマーでは、
残り秒数を毎秒1減らす
だけではなく、
終了予定時刻を保存して現在時刻との差から残り時間を計算する
方式も検討できます。
例えば、
const endTime = Date.now() + timeLeft * 1000;
として終了予定時刻を保存します。
その後、
const remaining = Math.max(
0,
Math.ceil((endTime - Date.now()) / 1000)
);
のように残り時間を計算します。
この方法なら、intervalが多少遅れても実際の時計との差が広がりにくくなります。
タブを開き直しても続けたい場合
ブラウザを再読み込みするとReactのstateは初期化されます。
タイマー状態を保持したい場合は、
localStorage
などへ保存する方法があります。
例えば、
- 現在のモード
- 終了予定時刻
- タイマー実行中か
- 完了セット数
などを保存します。
ページを開いたときに読み込めば、タイマーを復元できます。
useRefを使う方法
タイマーを発展させると、interval IDやレンダーに直接必要のない値を保持したくなる場合があります。
そのような値にはuseRefを利用できます。
useRefは、レンダーをまたいで値を保持しつつ、その値を変更しても再レンダーを発生させたくない場合に利用できます。
例えば、
interval ID
などの保持に使えます。
基本的には、
画面表示に必要な値はstate
画面表示に直接必要ない値はref
と分けると分かりやすいでしょう。
タイマー処理をCustom Hookにする
タイマーのコードが大きくなってきたら、
usePomodoroTimer()
のようなCustom Hookへ切り出す方法があります。
例えば、
const {
mode,
timeLeft,
isRunning,
start,
pause,
reset,
skip,
} = usePomodoroTimer();
という形です。
UIとタイマー処理を分離できるため、コンポーネントが読みやすくなります。
タイトルに残り時間を表示する
Web版ポモドーロタイマーでは、ブラウザのタブに残り時間を表示すると便利です。
例えば、
useEffect(() => {
const minutes = Math.floor(timeLeft / 60);
const seconds = timeLeft % 60;
document.title =
`${minutes}:${String(seconds).padStart(2, "0")} | Pomodoro`;
}, [timeLeft]);
とします。
別タブで作業していても残り時間を確認できます。
終了音を追加する
タイマーが終了したら、音で知らせることもできます。
ただし、ブラウザでは音声の自動再生が制限される場合があります。
ユーザー操作後に音声を利用できる状態にしておくなど、ブラウザの仕様を考慮して実装しましょう。
また、
- 音量
- ミュート
- 通知ON/OFF
を設定できるようにすると使いやすくなります。
通知を使う
ブラウザのNotifications APIを利用して、
集中時間が終了しました
などの通知を表示する方法もあります。
ただし、通知にはユーザーの許可が必要です。
最初から許可を要求するのではなく、通知機能を有効にしたいユーザーが操作したタイミングで許可を求める設計にすると分かりやすいでしょう。
Reactでポモドーロタイマーを作ると学べること
ポモドーロタイマーは、Reactの練習題材として多くの要素を扱えます。
例えば、
useStateuseEffectuseRef- イベントハンドラ
- cleanup
- 条件分岐
- Custom Hook
- localStorage
- Web Notifications
- コンポーネント分割
などです。
最初は25分をカウントダウンするだけにして、少しずつ機能を追加すると理解しやすくなります。
最初から多機能にしない
最初から、
- ログイン
- タスク管理
- 統計
- クラウド同期
- BGM
- テーマ
- 通知
- 自動開始
まで実装すると複雑になります。
まず、
25分が減る
↓
停止できる
↓
リセットできる
↓
5分休憩へ切り替わる
ところまで作りましょう。
そのあとで機能を追加するとデバッグしやすくなります。
完成済みのポモドーロタイマーを試す
自分でReactを使って実装する前に、実際のポモドーロタイマーを触ってみる方法もあります。
Pomolikaはブラウザで使えるポモドーロタイマーです。
実際にタイマーを操作して、
- 開始
- 停止
- 再開
- リセット
- 集中と休憩の切り替え
などの動きを確認すると、自分で実装するときのUI設計の参考にもなります。
まとめ
Reactでは、useStateとuseEffectを使ってポモドーロタイマーを実装できます。
基本的な流れは、
- 残り時間をstateで管理する
- 実行中かどうかをstateで管理する
useEffectでタイマーを開始するsetIntervalで時間を更新する- cleanupで
clearIntervalする - 0になったら集中と休憩を切り替える
です。
基本機能が完成したら、
- 長い休憩
- 自動切り替え
- スキップ
- 時間変更
- タブへの残り時間表示
- localStorage
- 通知
- 終了音
などを追加できます。
特に実用的なタイマーを作る場合は、単純に1秒ずつstateを減らすだけでなく、
終了予定時刻と現在時刻から残り時間を計算する
方法も検討しましょう。
まずはシンプルな25分・5分タイマーから作り、少しずつ機能を追加していくのがおすすめです。