Reactで作るカウントダウンタイマー

# はじめに
今回はこちらのタイマー作成のお題にチャレンジしました。


https://qiita.com/Sicut_study/items/1e4d8ec06f1af42a47e5?utm_campaign=post_article&utm_medium=twitter&utm_source=twitter_share

基礎学習を終えて【Reactアプリ100本ノック】を利用して何かを作るということをひたすらに繰り返しています。
タイマー作成を終えて実装の中身や大変だったところを記録として残せたらなと思っております。

# 仕様
・分と秒を入力してタイマーを設定
・設定された時間からカウントダウンが始まる
・スタート、停止、再開、リセットの機能を付ける
・時間が0になったとき効果音で通知する

# 使用技術
・React
・TypeScript
・Chakra-UI
・use-sound

# 完成図

![スクリーンショット 2024-07-25 21.21.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2120218/26cdeff3-77b4-2ada-f1b2-1d85896ec4a1.png)


# ソースコード
### メインコンポーネント
このコンポーネントは、タイマーの状態を管理し、ユーザー入力を受け取り、カウントダウンを制御します。
```react:CountdownTimer.tsx
import React, { useState, useEffect } from 'react';
import {
  Box,
  Text,
  VStack,
  Heading,
  useToast,
} from '@chakra-ui/react';
import { TimerButtons } from './TimerButtons';
import useSound from 'use-sound';
import { InputTime } from './InputTime';


export const CountdownTimer: React.FC = () => {
  const [minutes, setMinutes] = useState<number>(0);
  const [seconds, setSeconds] = useState<number>(0);
  const [isActive, setIsActive] = useState<boolean>(false);
  const [timeLeft, setTimeLeft] = useState<number>(0);
  const [isAlarmPlayed, setIsAlarmPlayed] = useState<boolean>(false); // 効果音が再生されたかどうかの状態
  const toast = useToast();

  // 効果音の設定
  const [play] = useSound("/alerm-sound.mp3")

  // カウントダウンを管理するuseEffect
  useEffect(() => {
    let timer: NodeJS.Timeout;
    if (isActive && timeLeft > 0) {
      timer = setInterval(() => {
        setTimeLeft(prev => prev - 1);
      }, 1000);
    } else if (timeLeft === 0 && isActive) {
      if (!isAlarmPlayed) {
        play(); // 効果音を再生
        setIsAlarmPlayed(true); // 効果音が再生されたことを記録
      }
      setIsActive(false); // タイマーを停止
    }
    return () => clearInterval(timer);
  }, [isActive, timeLeft, play, isAlarmPlayed]);

  // スタートボタンを押したときの処理
  const handleStart = () => {
    const totalSeconds = minutes * 60 + seconds;
    if (totalSeconds <= 0 || totalSeconds > 3600) {
      toast({
        title: '無効な時間',
        description: '0より大きく、60分以内の値を入力してください。',
        status: 'error',
        duration: 3000,
        isClosable: true,
      });
      return;
    }
    setTimeLeft(totalSeconds);
    setIsActive(true);
  };
  // 一時停止ボタンを押したときの処理
  const handlePause = () => setIsActive(false);

  // 再開ボタンを押したときの処理
  const handleResume = () => setIsActive(true);

  // リセットボタンを押したときの処理
  const handleReset = () => {
    setIsActive(false);
    setTimeLeft(minutes * 60 + seconds);
  };

  return (
    <Box textAlign="center" p={5}>
      <Heading mb={20}>カウントダウンタイマー</Heading>
      <VStack spacing={4} >
        <InputTime
          minutes={minutes}
          seconds={seconds}
          setMinutes={setMinutes}
          setSeconds={setSeconds}/>

        <TimerButtons
          onStart={handleStart}
          onPause={handlePause}
          onResume={handleResume}
          onReset={handleReset}
        />
        <Text fontSize="60px">
          {Math.floor(timeLeft / 60)}:{String(timeLeft % 60).padStart(2, '0')}
        </Text>
      </VStack>
    </Box>
  );
};
```

今回の。タイマーが0に達したときにのみ効果音を鳴らす実装はuseSoundフックを使用して実現しました。
```react:
} else if (timeLeft === 0 && isActive) {
      if (!isAlarmPlayed) {
        play(); // 効果音を再生
        setIsAlarmPlayed(true); // 効果音が再生されたことを記録
      }
      setIsActive(false); // タイマーを停止
```
上の部分でタイマーが0になり、アクティブな状態であることと効果音がまだ再生されていないことを確認しています。
この条件により、カウントダウンが0になったときにのみ効果音が再生されます。また、効果音が再生された後はタイマーを停止し、再度効果音が鳴らないようにします。

### TimeInputコンポーネント

```react:TimeInput.tsx
import { HStack, Input, Text } from "@chakra-ui/react";
import { FC } from "react";

type Props = {
  minutes: number;
  seconds: number
  setMinutes: (value: number) => void
  setSeconds: (value: number) => void
}

export const InputTime: FC<Props> = (
  { minutes, seconds, setMinutes, setSeconds }
) => {

  return (
    <HStack spacing='24px' mb={20}>
      <Text>分</Text>
      <Input
        type="number"
        value={minutes}
        onChange={(e) => setMinutes(Number(e.target.value))}
        placeholder="分"
        width="100px"
      />
      <Text>秒</Text>
      <Input
        type="number"
        value={seconds}
        onChange={(e) => setSeconds(Number(e.target.value))}
        placeholder="秒"
        width="100px"
      />
    </HStack>
  )
}

```

このコンポーネントは、setMinutesとsetSecondsという関数をプロップスとして受け取り、入力値が変更されるたびにこれらの関数を呼び出して親コンポーネントに新しい値を渡します。

## TimerButtons

```react:TimerButtons.rtsx

import React from 'react';
import { HStack, Button } from '@chakra-ui/react';

interface TimerButtonsProps {
    onStart: () => void;
    onPause: () => void;
    onResume: () => void;
    onReset: () => void;
}

export const TimerButtons: React.FC<TimerButtonsProps> = ({ onStart, onPause, onResume, onReset }) => {
    return (
        <HStack spacing="24px">
            <Button colorScheme="teal" onClick={onStart}>スタート</Button>
            <Button colorScheme="orange" onClick={onPause}>一時停止</Button>
            <Button colorScheme="blue" onClick={onResume}>再開</Button>
            <Button colorScheme="red" onClick={onReset}>リセット</Button>
        </HStack>
    );
};

```

このコンポーネントは、各ボタンがクリックされたときに親コンポーネントから渡されたハンドラー関数を呼び出してタイマー状態を制御します。

## 実装で苦労したこと

1.効果音の設定
タイマーが０になったとき効果音を鳴らす実装に手こずりました。
タイマーの実装は以下を参考にしてます。
[React | use-sound を利用した音声再生方法](https://prettytabby.com/react-sound/)


2.タイマーの状態管理
タイマーの開始、停止、再開、リセットの各状態を適切に管理すること苦労しました。
各ボタンの実装処理をどう実現すれば良いのを考えることに時間を使いました。

## まとめ

基礎学習で学んだことを活かしつつ効果音の設定など新しいことも学べました。
状態管理や値の受け渡しなど基礎的な部分をアウトプットを通じて固めていきたいと思います。