⏲️

Reactでタイマーアプリを作る

2024/12/07に公開

概要

Reactの学習を兼ねてタイマーアプリを作成したので、備忘録として残しておきます。

技術スタック

  • React
  • shadcn/ui
  • vite

機能要件

  • 時、分、秒を表示し、それぞれ加算、減算できる。時は00~23まで、分は00~59まで、秒は00~59まで。
  • スタートボタン、ストップボタン、クリアボタンを作成する。
  • スタートボタンを押すと、設定した時間が減っていき、0になると音楽がなる。
  • ストップボタンを押すと、タイマーがストップする。
  • クリアボタンを押すと、設定した時間がすべて消える。

※あとから細かい部分は修正します。

実装

環境構築

Viteを使って環境構築をします。

https://ja.vite.dev/guide/

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: » ReactSelect a variant: » TypeScript

プロジェクトが作成できたら、

cd timer-app
yarn install
yarn dev

続いてshadcn/uiをインストールします。
下記のドキュメントを参照してください。
https://ui.shadcn.com/docs/installation/vite

実装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]);

クリーンアップ関数については、こちらの記事を参考にしました。
https://zenn.dev/arisa_dev/books/reactjs-hooks/viewer/chapter11

実装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の記事についても初めての投稿になるのですが、記事を書くことで、学んだことを整理できるので、自己学習のためにとても有用だと思いました。
これからもがんばって投稿していきます。

今回の実装に関しては、下記の記事を参考にさせていただきました。
https://qiita.com/ogikazuhiro/items/048cad7c865a91682697

実装に関して問題や改善点があれば、コメントいただけるとありがたいです。

Create Quest.Inc Tech Blog

Discussion