Reactでタイマーアプリを作る
概要
Reactの学習を兼ねてタイマーアプリを作成したので、備忘録として残しておきます。
技術スタック
- React
- shadcn/ui
- vite
機能要件
- 時、分、秒を表示し、それぞれ加算、減算できる。時は00~23まで、分は00~59まで、秒は00~59まで。
- スタートボタン、ストップボタン、クリアボタンを作成する。
- スタートボタンを押すと、設定した時間が減っていき、0になると音楽がなる。
- ストップボタンを押すと、タイマーがストップする。
- クリアボタンを押すと、設定した時間がすべて消える。
※あとから細かい部分は修正します。
実装
環境構築
Viteを使って環境構築をします。
yarn create vite
yarn create v1.22.22
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-vite@5.5.5" with binaries:
- create-vite
- cva
√ Project name: ... timer-app
√ Select a framework: » React
√ Select a variant: » TypeScript
プロジェクトが作成できたら、
cd timer-app
yarn install
yarn dev
続いてshadcn/uiをインストールします。
下記のドキュメントを参照してください。
実装1:時間を増減するボタンの作成
まずは、時は1時間、分は1分、秒は1秒加算、減算できるボタンを作成します。
function App() {
return (
<>
<button>+1秒</button>
<button>-1秒</button>
<button>+1分</button>
<button>-1分</button>
<button>+1時間</button>
<button>-1時間</button>
</>
)
}
export default App
次に、時間を保持するステートを作成し、値を増減する関数をそれぞれ作成します。
引数で受け取った値でtimerCountを増減します。
const [timerCount,setTimerCount] = useState(0)
const plus = (plusCount:number)=>{
setTimerCount((prevVal)=>prevVal + plusCount)
}
const minus = (minusCount:number) =>{
setTimerCount((prevVal)=>prevVal + minusCount)
}
今回はミリ秒でタイマーのカウントを管理し、カウントが更新されるたびに"00:00:00"の形にフォーマットして出力するというやり方で実装します。
1時間、1分、1秒をミリ秒に変換した値を定数として定義しておき、
先ほど作成した、値を増減する関数をbuttonタグのonClickにセットします。
関数の引数には、定義しておいた定数をセットします。
import { useState } from "react";
const ONE_HOURS = 3600000;
const ONE_MINUTES = 60000;
const ONE_SECONDS = 1000;
function App() {
const [timerCount, setTimerCount] = useState(0);
const plus = (plusCount: number) => {
setTimerCount((prevVal) => prevVal + plusCount);
};
const minus = (minusCount: number) => {
setTimerCount((prevVal) => prevVal - minusCount);
};
return (
<div>
<div>{timerCount}</div>
<div>
<button onClick={() => plus(ONE_SECONDS)}>+1秒</button>
<button onClick={() => minus(ONE_SECONDS)}>-1秒</button>
<button onClick={() => plus(ONE_MINUTES)}>+1分</button>
<button onClick={() => minus(ONE_MINUTES)}>-1分</button>
<button onClick={() => plus(ONE_HOURS)}>+1時間</button>
<button onClick={() => minus(ONE_HOURS)}>-1時間</button>
</div>
</div>
);
}
export default App;
時間の増減ができるようになりました。
最後に値の上限と下限をplus、minus関数に設定します。
上限は、24時間(=86400000ms) 下限は0です。
const plus = (plusCount: number) => {
if(timerCount + plusCount <= MAX_COUNT){
setTimerCount((prevVal) => prevVal + plusCount);
}
};
const minus = (minusCount: number) => {
if(timerCount - minusCount >= MIN_COUNT){
setTimerCount((prevVal) => prevVal - minusCount);
}
};
実装2:スタート、ストップ、リセットボタンの作成
スタート、ストップ、リセットボタンを作成し、それぞれに関数をセットしておきます。
const start = ()=>{
}
const stop = ()=>{
}
const reset = ()=>{
}
return (
<div>
<div>{timerCount}</div>
<div>
<button onClick={() => plus(ONE_SECONDS)}>+1秒</button>
<button onClick={() => minus(ONE_SECONDS)}>-1秒</button>
<button onClick={() => plus(ONE_MINUTES)}>+1分</button>
<button onClick={() => minus(ONE_MINUTES)}>-1分</button>
<button onClick={() => plus(ONE_HOURS)}>+1時間</button>
<button onClick={() => minus(ONE_HOURS)}>-1時間</button>
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
</div>
);
タイマーの状態を定義してあげます。
今回は、Active(タイマー起動状態)、StandBy(タイマー停止状態)、End(タイマー終了状態)の3つで状態を管理します。
Startボタンを押すとActive状態になり、Stop、Resetボタンを押すと、StandBy状態になるようにします。End状態は後ほど登場します。
const [timerState,setTimerState] = useState('standby')
~~
const start = ()=>{
setTimerState('active')
}
const stop = ()=>{
setTimerState('standby')
}
const reset = ()=>{
setTimerState('standby')
setTimerCount(0)
}
実装3:タイマーのロジック部分の作成
タイマーのロジック部分になります。
setInterval関数を使用し、タイマーカウントを1秒ずつ減らします。
useEffect(() => {
//タイマーの状態がActiveの時は早期リターン
if (timerState !== "active") {
return;
}
//タイマーのカウントが0を超えているときタイマーをスタート
if (timerCount > 0) {
//setIntervalで1秒(1000ms)おきに、タイマーのカウントを減らす
//setIntervalのIDをRefに保持しておく
timerIdRef.current = setInterval(() => {
setTimerCount((prevVal) => prevVal - ONE_SECONDS);
}, ONE_SECONDS);
} else {
//タイマーカウントが0になったら、clearIntervalでタイマーを止め、タイマーの状態をEndにする。
clearInterval(timerIdRef.current);
setTimerState("end");
}
//クリーンアップ関数 useEffectが再実行される前や、コンポーネントのアンマウント時に呼び出され、古いタイマーを停止する。
//これがないと、ストップやリセットボタンを押しても、タイマーが止まらない。
return () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
};
}, [timerState, timerCount]);
クリーンアップ関数については、こちらの記事を参考にしました。
実装4:ミリ秒をフォーマット
ここまでミリ秒で加算や減算を行っていたので、ミリ秒を'00:00:00'の形にフォーマットします。
formatTime関数を作成します。
const formatTime = (milliseconds:number)=>{
const hh = Math.floor(milliseconds / ONE_HOURS);
const mm = Math.floor((milliseconds % ONE_HOURS)/ONE_MINUTES)
const ss = Math.floor((milliseconds % ONE_MINUTES)/ONE_SECONDS)
return [hh,mm,ss].map((val)=>String(val).padStart(2,'0')).join(':')
}
引数でタイマーカウントを受け取り、時、分、秒をそれぞれ計算して求めます。
mmを例にすると、最初にミリ秒を1時間で割ることで1時間未満である余りのミリ秒数を取得します。
次に1時間未満のミリ秒数を1分(60000ミリ秒)で割ることで、分数を求めます。
最後にMath.floorで分数を切り捨てし、整数部分のみを取得します。
戻り値ではhh、mm、ssをそれぞれpadStart関数を使って0埋めし、join関数で':'で結合する処理を書いています。
デザインを整え、最終的には下図のようになりました。
実装5:タイマーが0になった時の音楽
最後にタイマーが0になったタイミングで音楽を鳴らすようにします。
Audioオブジェクトを使って、音楽を再生します。
useEffect(() => {
//Audioインスタンスを初期化
audioRef.current = new Audio("/sound/alarm.mp3");
//クリーンアップ関数 アンマウント時余計なリソースを開放する。
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
}, []);
const playSound = () => {
if (audioRef.current) audioRef.current.play();
};
const stopSound = () => {
setTimerState("standby");
if (audioRef.current) {
audioRef.current.pause(); //音楽を一時停止
audioRef.current.currentTime = 0; //再生位置を最初に戻す
}
};
useEffect(() => {
...
} else {
...
setTimerState("end");
playSound(); //追加
}
...
}, [timerState, timerCount]);
return(
<div>
...
{/* 音楽停止用ボタン */}
<div className="flex justify-center space-x-2">
<Button onClick={stopSound}>Stop Alarm</Button>
</div>
)
まとめ
Reactの学習のため、手始めにタイマーアプリを実装してみました。
ロジック部分の実装は、シンプルながら他の部分でも活用できる内容だと思うので、他にアプリケーションを作成する際には、積極的に応用していきたいです。
Zennの記事についても初めての投稿になるのですが、記事を書くことで、学んだことを整理できるので、自己学習のためにとても有用だと思いました。
これからもがんばって投稿していきます。
今回の実装に関しては、下記の記事を参考にさせていただきました。
実装に関して問題や改善点があれば、コメントいただけるとありがたいです。
Discussion