今回やること
ViteでReactプロジェクトを作り、開始・停止・リセットができるタイマーを実装します。
useStateで秒数と実行状態を持つ- クリックイベントでstateを更新する
- stateから表示用の派生値を計算する
useEffectでintervalを開始し、cleanupで停止する
Context、Reducer、データ取得、最適化用Hooksは扱いません。最初に必要な2つのHooksへ集中します。
前提条件
- Node.js 20.19以上、または22.12以上
- npm
- Reactのコンポーネントとpropsが分かる
Viteプロジェクトを作る
npm create vite@latest react-hooks-timer -- --template react-ts
cd react-hooks-timer
npm install
npm run dev
ターミナルに表示されたURLを開き、Viteの初期画面が表示されることを確認します。
タイマーを実装する
src/App.tsxを次の内容にします。
import { useEffect, useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) {
return;
}
const intervalId = window.setInterval(() => {
setSeconds((current) => current + 1);
}, 1_000);
return () => {
window.clearInterval(intervalId);
};
}, [isRunning]);
const status = isRunning ? "計測中" : "停止中";
const isLongSession = seconds >= 10;
function handleReset() {
setIsRunning(false);
setSeconds(0);
}
return (
<main>
<h1>学習タイマー</h1>
<p aria-live="polite">経過時間: {seconds}秒</p>
<p>状態: {status}</p>
{isLongSession && <p>10秒続きました!</p>}
<button type="button" onClick={() => setIsRunning(true)}>
開始
</button>
<button type="button" onClick={() => setIsRunning(false)}>
停止
</button>
<button type="button" onClick={handleReset}>
リセット
</button>
</main>
);
}
コードの要点
stateとイベント
useStateは現在の値と更新関数を返します。ボタンのonClickは更新関数を呼び、Reactへ再表示を依頼します。
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
interval内ではsetSeconds((current) => current + 1)という更新用関数を使います。古いsecondsを閉じ込めず、直前の値から安全に1を足せます。
派生値はrender中に計算する
statusとisLongSessionは、既存のstateから計算できます。別のstateやEffectへ複製する必要はありません。
const status = isRunning ? "計測中" : "停止中";
const isLongSession = seconds >= 10;
stateを必要最小限にすると、値同士のずれを防げます。
Effectとcleanup
intervalはReactの外側で時間を刻む仕組みなので、useEffectで同期します。isRunningが変わる前とコンポーネントが画面から消える時に、cleanupが古いintervalを停止します。
Viteの初期構成は開発時にStrict Modeを使います。Strict Modeでは問題を見つけるため、Effectのsetup → cleanup → setupが追加で実行されます。このコードはsetupごとに作ったintervalIdを対応するcleanupで必ず停止するため、intervalが重複しません。
成功を確認する
開発serverが止まっている場合は起動します。
npm run dev
ブラウザで次を確認してください。
- 初期表示が0秒・停止中になる
- 「開始」を押すと約1秒ごとに数値が増える
- 「停止」を押すと数値が増えなくなる
- 10秒になるとメッセージが表示される
- 「リセット」で0秒・停止中へ戻る
約1秒で2ずつ増える場合は、cleanupが消えていないか確認します。
よくあるエラー
数値が増え続けて停止できない
Effectからwindow.clearInterval(intervalId)を行うcleanupを返しているか確認します。
1秒ごとにintervalが作り直される
Effect内でsetSeconds(seconds + 1)と書き、依存配列へsecondsを入れている可能性があります。更新用関数setSeconds((current) => current + 1)を使い、依存値をisRunningだけにします。
Hookを条件分岐の中で呼んでいる
useStateとuseEffectはコンポーネントの最上位で呼びます。条件分岐はEffectの内側へ置きます。
練習
「+10秒」ボタンを追加し、setSeconds((current) => current + 10)で秒数を増やしてください。タイマーの実行中と停止中の両方で動くことを確認します。
次のステップ
タイマーをテストしたくなったら、まずReact Testing Library実践で利用者操作を基準にしたテストを学んでください。
参考リソース
- React: useState
- React: useEffect
- React: Synchronizing with Effects
- React: StrictMode
- Vite: Getting Started
(最終確認: 2026年7月25日)
← 一覧に戻る