【Reactアプリ100本ノック】04 Timer

## はじめに
ゆかっしゅです。
2021年に事務職からデザイナー/コーダーにジョブチェンジをし、現在はフロントエンドエンジニアにスキルアップするべく、モダンフロントエンドを学習しています。

ReactやTypeScriptの基礎学習は終了したのに、なかなか0から自分だけでアプリを作れないので手を動かすべく「Reactアプリ100本ノック」 に挑戦してみようと思います。

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

## Reactアプリ100本ノックルール
- 主要なライブラリやフレームワークはReactである必要がありますが、その他のツールやライブラリ（例: Redux, Next.js, Styled Componentsなど）を組み合わせて使用することは自由
- TypeScriptを利用する
- 要件をみたせばデザインなどは自由
- なるべくChatGPTなどのAIツールに頼らない（自分独自ルール）

## 04. Timer
![スクリーンショット 2025-04-28 113605.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4068307/2bf4c48d-2389-4fd5-b5ba-8c7d859689bd.png)


## 問題
タイマーアプリを作成します

## 目的
タイマーアプリでは、Reactの根幹をなす状態管理、ライフサイクルメソッド、そしてイベント処理の実践を通じて、Reactの基礎を固めます

またsetTimeoutなど指定した時間に何かを実行するという関数を学んでいきます

## 達成条件
1\.ユーザーは分と秒を入力してタイマーを設定できる。
2\.スタートボタンを押すとカウントダウンが開始される。
3\.時間が0になったら、ユーザーに通知される (効果音が鳴る)
4\.一時停止ボタンを押すとカウントダウンが停止し、再開ボタンでカウントダウンが再開される。
5\.リセットボタンでタイマーが設定時間に戻る。
6\.無効な時間（例：負の時間、非数値、60分以上の値）が入力された場合、エラーメッセージが表示される。

## 実際に解いてみた
### 利用技術
React(19.0.0)
TypeScript(5.0)
Next.js(15.3.1)
Tailwindcss(4.0)
jotai(2.12.3)
Vercel

解答時間:5.5時間


すごく時間がかかってしまいました....。
初めてタイマーアプリ作ったのですが、まだuseEffectをしっかり落とし込めてないなぁということを再確認できました。
また、状態管理に関しては教えていただいた「Jotai」を早速使ってみました！
調べながらやったのでさらに時間がかかりました。
また、レンダリング最適化もやってみました！


### リンク
https://react-app-100.vercel.app/

## Jotaiを使用してみて
RecoilがReactv19に対応していないことがわかり、代わりの状態管理を探していたところ
Jotaiを教えてもらったので使ってみました。
とても使いやすいです！！！！！
今後はこのJotaiを使用して状態管理をしてみようと思います。

https://zenn.dev/cocomina/articles/how-to-use-jotai


### 一つだけつまずいた点

```typescript:input.tsx

"use client";

import { useAtom } from "jotai";
import { PrimaryButton } from "./components/primaryButton";
import { minutesAtom, secondsAtom, startTimer, timerActive } from "./atom";
import { useState } from "react";

export default function Input() {
  const [minutes, setMinutes] = useAtom(minutesAtom);
  const [seconds, setSeconds] = useAtom(secondsAtom);
  const [, setActive] = useAtom(timerActive);
  const [, setStart] = useAtom(startTimer);

  const [error, setError] = useState("");

  const handleStart = () => {
    if (minutes < 0 || minutes > 99) {
      setError("分は0〜99の範囲で入力してください");
      return;
    }
    if (seconds < 0 || seconds > 59 || seconds === 0) {
      setError("秒は0〜59の範囲で入力してください");
      return;
    }

    setError("正しい数値（時間）を入力してください");
    setActive(true);
    setStart(true);
  };

  const handleMinutesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = Number(e.target.value);
    if (value >= 0 && value <= 99) {
      setMinutes(value);
    }
  };

  const handleSecondsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = Number(e.target.value);
    if (value >= 0 && value <= 59) {
      setSeconds(value);
    }
  };

  return (
    <div className="flex flex-col gap-25 items-center">
      <div className="flex gap-2 font-bold text-3xl">
        <div>
          <input
            type="number"
            min={0}
            max={99}
            value={minutes}
            onChange={handleMinutesChange}
            className="w-16 border-solid border-gray-300 border-2 rounded-md p-1"
          />
          分
        </div>
        <div>
          <input
            type="number"
            min={0}
            max={59}
            value={seconds}
            onChange={handleSecondsChange}
            className="w-16 border-solid border-gray-300 border-2 rounded-md p-1"
          />
          秒
        </div>
      </div>
      {error && <p className="text-red-500 mt-2">{error}</p>}
      <PrimaryButton handleStart={handleStart}>スタート</PrimaryButton>
    </div>
  );
}

```


`const [active, setActive] = useAtom(timerActive);`
値は使わず更新関数だけ使用したい時、最初上記のように記載していたのですが
ESLintで「これ使ってないよ！」っというエラーが出てしまいどのように書けばいいのかわからずちょっと躓きました。

結果は下記のように書くとエラーが出ずスムーズに実装できました。
`const [, setActive] = useAtom(timerActive);`


## React developer toolsと再レンダリング
今回たくさんのStateを作成したので、「無駄な再レンダリング起きてそうだなぁ。そもそも再レンダリングしてるところ簡単に調べられるツールないかなぁ。」と思って探したところ、React developer toolsを見つけました。
もともとChromにはインストールしてあったんですが使いどころがわからず眠っていたこのツール。
ざっくりとした使い方を下記に記載します。

### React developer tools
検証モードを開いて、コンソールの横にある>>をクリックしてProfilerをクリックします。
（Reactで作られてるアプリやサイトでないとこのボタンは表示されないみたいです）
![スクリーンショット 2025-04-28 142323.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4068307/f383069c-3d6f-4463-8e0c-ce1ae1c390e3.png)


そして歯車ボタンを押して「Highlight updates when components render.」のところにチェックをいれます。
![スクリーンショット 2025-04-28 142143.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4068307/0b6e52c0-e29d-423e-9444-05c69fa818e1.png)

そしてアプリを動かしてみると...

![スクリーンショット 2025-04-28 141756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4068307/97001971-cc6d-4435-8a2b-6b54151ef166.png)

再レンダリングしている箇所が緑色の四角に囲われて表示されます。
画像の場合はコンポーネントとして作成された`SecondaryButton`と`StopButton`が１秒減るごとに再レンダリングされていました。
ここでは秒数のみ再レンダリングして欲しく、ボタンは再レンダリングの必要がないのでmemo化します。
memo化した後の画像はコチラ。

![スクリーンショット 2025-04-28 142649.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4068307/9aa692b0-a7d7-41a2-ae96-6023443ba97b.png)


ボタンが再レンダリングされなくなりました！



## 終わりに

とても時間がかかってしまいましたが、useEffectを始めJotaiの使い方やパフォーマンスチューニングについて学べてとてもいい学習になりました。