公開したタイマーが使えなかったので原因究明
はじめに
Reactでポモドーロタイマーを作ってGitHub Pagesで公開しました
基本的な機能は大丈夫だし できた! と思って、実際にスタートしてから作業を始めました
すると、25分なはずなのに30分経っても、1時間経っても通知が来ません
カウントダウンが全然進んでないんですよ
原因:ブラウザ
原因はReactやGitHub Pagesではなく、ブラウザだとわかりました
ブラウザはユーザーが別のタブや別のアプリに切り替えたりすると、非アクティブなタブやウィンドウでのJavaScriptの実行頻度を自動的に低下させることがあります
このためバックグラウンドで動作させようとしてもタイマーが期待通りに進まないということです
使用技術
- React
- TypeScript
- Vite
- TailwindCSS
元々の実装方法
まずこれを作ろうとしたきっかけがReactの基礎固めをしたいということでした
そのため、最初は色々と調べました
すると、setInterval
を使っている記事がいくつも出てきたので、なんとなくは知ってるやつだしこれで作れるのかーと実装していきました
setInterval
についてはこちらをご覧ください
スタートボタンを押した時の処理
(無駄なステートが多いのは今回は見逃してください…)
const intervalID = useRef<number>();
const startTime = useCallback((): void => {
intervalID.current = setInterval(() => tick(), 1000);
setIsStart(true);
setIsStop(false);
setIsTimeUp(false);
setIsReset(false);
}, []);
ここでは、1秒(1000ms)ごとにtick
という関数を実行していますね
以下のコードがtick
関数ですが、これは条件ごとにtimeLimit
を更新するための関数です
const tick = useCallback(() => {
setTimeLimit((prevTimeLimit) => {
const newTimeLimit = Object.assign({}, prevTimeLimit);
const { min, sec } = newTimeLimit;
if (min <= 0 && sec <= 0) {
stopTime();
setIsTimeUp(true);
return newTimeLimit;
}
if (newTimeLimit.min > 0 && newTimeLimit.sec <= 0) {
newTimeLimit.min -= 1;
newTimeLimit.sec = 60;
}
newTimeLimit.sec -= 1;
return {
min: zeroPaddingNum(newTimeLimit.min),
sec: zeroPaddingNum(newTimeLimit.sec),
};
});
}, []);
ストップ&リセットの処理
どちらもclearInterval
で1秒ごとに繰り返してた処理を取り消しています
つまりタイマーが止まります
const stopTime = useCallback(() => {
clearInterval(intervalID.current);
setIsStop(true);
setIsStart(false);
}, []);
const resetTime = useCallback(() => {
clearInterval(intervalID.current);
setTimeLimit(time);
setIsReset(true);
setIsStart(false);
setIsStop(false);
setIsTimeUp(false);
}, []);
これでももちろんちゃんとタブを開いていれば動作します
ただ作業中に使うためのタイマーなのでバックグラウンドで動いて欲しい!ってことで改善しました
改善した実装方法
元々のやつは一応残したまま、別のものとして新たに作りました
なのでデザインとかは違います
そして本題のどうやって解決したかですが、Web Workersという仕組みを使いました
まだ自分も理解しきれていないのですが今回の場合、タイマーの処理をメインの処理とは別のスレッドに移すことでバックグラウンドでのタイマーの実行を可能にするという感じです
実際のコード
以下がWeb Workersを使ったコードになります
実はほぼchat-gptに書いてもらったものですが…
postMessage
とonmessage
を使ってやり取りできるようです
useEffect(() => {
// Web Workerを初期化
workerRef.current = new Worker(
new URL("../timerWorker.ts", import.meta.url)
);
workerRef.current.onmessage = (event) => {
setTimeLeft(event.data.timeLeft);
};
return () => {
if (workerRef.current) {
workerRef.current.terminate();
}
};
}, []);
let timerId: number | undefined;
self.onmessage = (event) => {
const { command, endTime } = event.data;
if (command === "start") {
if (timerId) return; // 既に動作中なら無視
const interval = 1000;
timerId = setInterval(() => {
const currentTime = Date.now();
const timeLeft = Math.max(Math.floor((endTime - currentTime) / 1000), 0);
self.postMessage({ timeLeft });
if (timeLeft <= 0) {
clearInterval(timerId);
timerId = undefined;
}
}, interval);
} else if (command === "stop") {
clearInterval(timerId);
timerId = undefined;
}
};
あとはそれぞれのボタンに対応する関数を用意します
const startTimer = () => {
if (workerRef.current) {
const endTime = Date.now() + timeLeft * 1000; // 現在時刻 + 残り時間
workerRef.current.postMessage({ command: "start", endTime });
setIsRunning(true);
}
};
const stopTimer = () => {
if (workerRef.current) {
workerRef.current.postMessage({ command: "stop" });
setIsRunning(false);
}
};
const resetTimer = () => {
if (workerRef.current) {
workerRef.current.postMessage({ command: "stop" });
setIsRunning(false);
setTimeLeft(25 * 60);
}
};
冬なので雪を降らせた
こちらの記事を参考にタイマーが動いている間だけ雪を降らせるようにしてみました!
まとめ
開発している時は当たり前にずっと見ながら作業するから気づかなかったブラウザのバックグラウンドで実行させるにはどうすればいいか知れる良い機会でした
このpomotime-v2
はデザインをあまり気にする時間がなかったので、もう少し改善して使いやすくするのと、元々の方にはあったその日の合計を記録するやつとかも加えていこうかなと思います!
25分と5分だけだし、タイムアップ時には通知がくるだけ(音はならない)の単純なアプリですが、作業する際にぜひ雪を降らしてみてください
Discussion