1. はじめに
「Next.js × TypeScript 100本ノック」企画の4日目。この企画は、1日1アプリMVP完成を目標に、毎日異なる題材をもとにNext.js × TypeScriptで実装を行い、その過程で新しい技術やパターンを学んでいく100本ノック企画です。
前回の記事はこちら
本日はポモドーロ・タイマーを作りました。
Day3(ToDo Lite)では「状態をReactで扱う」「シンプルに保存する」の基本を練習。Day4は時間を扱うUI(タイマー)に踏み込み、useEffect
×setInterval
の安全な使い方、通知(Web Notifications)、そしてSSR/CSRの違いによるハマりを実体験で学びます。
今回のDay4の位置づけ
-
useEffect
のクリーンアップでタイマーを安全に制御 - 作業↔休憩サイクルとロングブレイクの状態遷移設計
- 通知(権限取得/発火)とタブタイトル更新
- 短い効果音(Web Audio) の最小実装
成果物一覧: https://knock-five.vercel.app/knocks
2. Day4の課題と目的
課題:
- 25分作業→5分休憩のポモドーロ・タイマー
- 開始 / 一時停止 / リセット / スキップ
- フェーズ切替で通知(任意)&サウンド(任意)
- 指定回数ごとにロングブレイク(例:4回ごと)
学習目的:
-
useEffect
とsetInterval
の副作用+クリーンアップパターン -
関数型アップデート(
setState(prev => ...)
)で安全に残り時間を更新 -
SSR安全なブラウザAPIの扱い(
window
/document
/Notification
) - 状態遷移の設計(phase: work/break + isLong + rounds管理)
3. 完成イメージ
4. 使用技術と依存ライブラリ
- Next.js (App Router) + React + TypeScript
- Tailwind CSS
- Web Notifications API(任意)
- Web Audio API(任意)
5. 実装コード全文
ソースコード全文はGitHubにあります:
matcha4smiley/knock
6. ハマりポイントと解決策
-
Hydration failed(SSRとCSRで表示がズレる)
→ 初回は必ず同じマークアップを出す(mounted
フラグで「判定中…」を表示)。Notification.permission
等のブラウザAPIは**useEffect
後にstateへ反映**して分岐。 -
タイマーの二重起動/リーク
→useEffect
でsetInterval
を張り、必ずクリーンアップでclearInterval
。 -
0秒チラつき & autoNextが効かない
→ フェーズ切替時に0を返さず次フェーズの秒数を返す。 -
Pauseで残りが満タンに戻る
→ 「停止中だけ設定反映」エフェクトは**差分判定(前回のphaseSeconds
と比較)**+[phaseSeconds, running]
依存で解決。 -
ESLint: missing deps(
fireNotification
/playBeep
/設定値など)
→ 関数はuseCallback
化、エフェクト依存に網羅的に追加。
7. 今回習得できた知識
-
useEffect
×setInterval
の定石(クリーンアップ/依存) - 関数型アップデートで競合を避けるタイマー更新
- SSR安全なクライアントAPIの扱い(mounted後に触る)
- 状態遷移設計(作業→休憩→作業、ロング条件判定、Skip/Resetの分岐)
- Web Notifications / Web Audio の実装
8. おわりに
時間を扱うUIは最初むずかしく見えるけど、やることはシンプル。
「1秒ごとに減らす → 0になったら次へ → 後片付け(clearInterval)」。この順番だけ確認すればOK。
迷ったら最小機能(開始・停止・リセット)から、動いたらサイクル→通知→音の順で少しずつ足していく。
-
useEffect
は「開始」と「片付け」をセットで書く場所。 - 何が変わったときに動くのかは依存配列でコントロール。
- うまくいかない時は、小さい機能(開始/停止/リセット)に分解して順番にテスト。
次回(Day5)は「電卓 — 四則演算と履歴」 を作ります。