【React】Hook ハンズオン


# 概要
![upload_bb4023ab6dddedf4fbb78e9a4c4c5132.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/e20528ce-58a2-d9e5-cf35-9a5e39513a0c.gif)

今回はストップウォッチアプリをハンズオン形式で作成します。
特に Hook にフォーカスを当てて、皆さんに学んでもらおうと思います。

# サンプルコード
github でコードを公開しています。
以下のコードを元にハンズオンを進めます。
- [App.js](https://github.com/ikeyuki0220/react-hook-hands-on/blob/main/App.js)
- [styles.css](https://github.com/ikeyuki0220/react-hook-hands-on/blob/main/styles.css)

# React Hook とは
Hook は React 16.8 で追加された新機能です。
state などの React の機能を、クラスを書かずに使えるようになります。

Hook 登場前の React ではクラスコンポーネントで書くのが一般的でした。

それはなぜかというと…
クラスコンポーネントには状態管理やライフサイクルの機能があるが、関数コンポーネントではそれらの機能がなかったからです。

しかし、フックがそれを変えました。
フックは状態管理やライフサイクルを扱うことができます。
つまり、フックを使用すればクラスコンポーネントでできていたことが、関数コンポーネントでもできるようになるのです。

引用）[フック早わかり](https://ja.legacy.reactjs.org/docs/hooks-overview.html)
引用）[5分でわかるReact Hooks](https://qiita.com/ushi_osushi/items/98d1bc4a83265a764084)

# 本ハンズオンで学べること
* コンポーネントを作成して Props を渡してみる
* Hook を触ってみる（useState／useRef／useEffect）
* カスタムHook を作成してみる

# 対象者
- HTML/CSS の経験がある。
- JavaScript の経験がある。
- React に興味があるがあまり触ったことがない。

# ハンズオンで触る技術の説明

## Props とは
[【React基礎】propsとは？親子コンポーネント受け渡し方をわかりやすく解説！](https://explore-zine.com/react-props/)

## useState／useRef／useEffect とは
[【備忘録】React Hooksの使い方まとめ](https://zenn.dev/snow_swallow/articles/2dd24c7955dc00)

:::note warn
今回触る useState／useRef／useEffect 以外の Hooks は見なくて OK です。
:::

## カスタムHook とは
[【React】カスタムフック(Custom Hook)を「完全に理解」してみる](https://qiita.com/mamimami0709/items/603c6ea9f9bfa68461f9)

# ハンズオンの準備
ハンズオンの環境ですが、WEBエディターで行います。
ですので、面倒な環境構築は一切不要です

1. [CodeSandbox](https://codesandbox.io/) にアクセスし、サインインする

:::note warn
サインインしないとコードを編集できないので注意です！
:::

2. 「Start for free」を押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/4b96cdf3-f1d9-123c-291e-825d2f98f839.png)

3. 「React」押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/4a1e2abe-4823-8004-f25c-8bde4c5862ab.png)

4. 「Open template」押下
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/9a53357b-24ed-b895-d606-ac6428036294.png)


5. App.js の不要な部分を削除する
    ```diff_jsx
    export default function App() {
    -   return (
    -     <div className="App">
    -       <h1>Hello CodeSandbox</h1>
    -       <h2>Start editing to see some magic happen!</h2>
    -     </div>
    -   );
    +   return <></>;
    }
    ```

6. styles.css の不要な部分を削除する
    ```diff_jsx
    - .App {
    -   font-family: sans-serif;
    -   text-align: center;
    - }
    ```

以上！

# ハンズオン開始

## 1. 実際にコンポーネントを作成して Props を渡してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/f67e6572-5ea3-d636-1244-f954973bd2a7.png)

こんな感じの画面を作成します。
あくまでガワだけでストップウォッチ機能は次章「Hooks を触ってみる」で実装します。

### ✅まずはラベルとボタンを表示する
App.js を以下のように修正して下さい。
全コピーして貼り付けで OK です。

```diff_jsx
import "./styles.css";

export default function App() {
  const time = 0;
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return (
    <div>
      <div>
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
      <div>
        <button onClick={() => alert("Start")}>Start</button>
        <button onClick={() => alert("Pause")}>Pause</button>
        <button onClick={() => alert("Reset")}>Reset</button>
      </div>
    </div>
  );
}
```

こんな感じで表示されれば OK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/ed14ad46-89d1-6062-4b0a-08d4babfeb27.png)

#### 📝解説

```jsx
const time = 0;
const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
const hours = `0${Math.floor(time / 3600000)}`.slice(-2);
```

ストップウォッチの時間(time)を、ミリ秒、秒、分、時間の単位に分割します。

```jsx
  return (
    <div>
      <div>
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
      <div>
        <button onClick={() => alert("Start")}>Start</button>
        <button onClick={() => alert("Pause")}>Pause</button>
        <button onClick={() => alert("Reset")}>Reset</button>
      </div>
    </div>
  );
```
ストップウォッチの UI を実装しています。

### ✅デザインをおしゃれにしてみる
ちょっとダサいのでデザインをおしゃれにしようと思います。
styles.css に以下を追加して下さい。

```css
.main-wrapper {
  height: 100vh;
  font-family: sans-serif;
  display: flex;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  gap: 20px;
}

.timer-label {
  font-size: xx-large;
}

.timer-button-container {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.timer-button {
  width: 120px;
  background-color: #fff;
  border: solid 2px #191970;
  color: #191970;
  border-radius: 20px;
  padding: 10px 30px;
  text-decoration: none;
  font-size: 1em;
  box-shadow: 0 5px 0 #191970;
  display: inline-block;
  transition: 0.3s;
}

.timer-button:hover {
  color: #191970;
  transform: translateY(5px);
  box-shadow: 0 0 0 #191970;
}

```

App.js を以下のように修正して下さい。

```diff_jsx
import "./styles.css";

export default function App() {
  const time = 0;
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return (
+   <div className="main-wrapper">
+     <div className="timer-label">
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
+     <div className="timer-button-container">
+       <button className="timer-button" onClick={() => alert("Start")}>
          Start
        </button>
+       <button className="timer-button" onClick={() => alert("Pause")}>
          Pause
        </button>
+       <button className="timer-button" onClick={() => alert("Reset")}>
          Reset
        </button>
      </div>
    </div>
  );
}
```

こんな感じで表示されれば OK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/c45a324e-6d47-8490-d023-b7a28edd76b8.png)

#### 📝解説
ここは CSS メインなので解説は割愛します。

### ✅ボタンを共通コンポーネント化して Props を渡してみる
ボタンの実装が冗長なので共通コンポーネント化してみます。
Props としてボタンのラベルとクリックイベントを受け取るようにします。

App.js に以下のコンポーネントを追加してください。

```diff_jsx
function TimerButton(props) {
  const { label, onClick } = props;
  return (
    <button className="timer-button" onClick={onClick}>
      {label}
    </button>
  );
}
```

App.js の `App` を以下のように修正して下さい。

```diff_jsx
import "./styles.css";

export default function App() {
  const time = 0;
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return (
   <div className="main-wrapper">
     <div className="timer-label">
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
     <div className="timer-button-container">
+       <TimerButton label="Start" onClick={() => alert("Start")} />
+       <TimerButton label="Pause" onClick={() => alert("Pause")} />
+       <TimerButton label="Reset" onClick={() => alert("Reset")} />
      </div>
    </div>
  );
}
```

先ほどと同様に表示されれば OK
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/ef0ec8fc-ed4c-e9ab-1046-1963252e0d39.png)

#### 📝解説
```jsx
function TimerButton(props) {
  const { label, onClick } = props;
  return (
    <button className="timer-button" onClick={onClick}>
      {label}
    </button>
  );
}
```
Props を受け取っているコンポーネントです。
ボタンのラベルとクリックイベントを受け取って、各 Props を設定した button タグを返却しています。

```jsx
<TimerButton label="Start" onClick={() => alert("Start")} />
<TimerButton label="Pause" onClick={() => alert("Pause")} />
<TimerButton label="Reset" onClick={() => alert("Reset")} />
```
`TimerButton` に Props を渡している部分です。

## 2. Hook を触ってみる（useState／useRef／useEffect）
![upload_bb4023ab6dddedf4fbb78e9a4c4c5132.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/11a86dd3-9ec4-4122-69c8-c2518183164b.gif)
useState、useRef、useEffect を用いて、ストップウォッチ機能を実装してみます。

### ✅ストップウォッチ機能を作成する
App.js を以下のように修正して下さい。

```diff_jsx
import "./styles.css";
+ import { useState, useRef, useEffect } from "react";

export default function App() {
+ const [time, setTime] = useState(0);
+ const intervalRef = useRef(null);
  
+ const handleStart = () => {
+   intervalRef.current = setInterval(() => {
+     setTime((prevTime) => prevTime + 10);
+   }, 10);
+ };

+ const handlePause = () => {
+   clearInterval(intervalRef.current);
+ };

+ const handleReset = () => {
+   clearInterval(intervalRef.current);
+   setTime(0);
+ };

+ useEffect(() => {
+   return () => {
+     clearInterval(intervalRef.current);
+   };
+ }, []);
  
- const time = 0;
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return (
    <div className="main-wrapper">
      <div className="timer-label">
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
      <div className="timer-button-container">
+       <TimerButton label="Start" onClick={handleStart} />
+       <TimerButton label="Pause" onClick={handlePause} />
+       <TimerButton label="Reset" onClick={handleReset} />
      </div>
    </div>
  );
}
```

こんな感じで表示されれば OK（活性／非活性の制御は次章で実施）
![upload_ecf4922353ff5f7a5e43749b97d9443e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806222/336980ff-f60a-1d9b-4956-56bbc47148d5.gif)


#### 📝解説
```diff_jsx
import { useState, useRef, useEffect } from "react";
```
import 文を使用して、useState／useRef／useEffect のフックをインポートしています。


```diff_jsx
const [time, setTime] = useState(0);
```
useState フックを使用して、`time` という状態を管理しています。
`time` はストップウォッチの経過時間を表します。
`setTime` はその状態を更新するための関数を返します。

```diff_jsx
const intervalRef = useRef(null);
```
useRef フックを使用して、`intervalRef` という新しい変数を定義しています。
`intervalRef` は、後続で説明する setInterval 関数で使用される interval ID を保存するために使用します。
`intervalRef` の更新に伴うレンダリングを発生させたくない（= UI に関係ない用途なのでレンダリングする必要がない）ので、useRef フックを用いています。

:::note info
**ポイント**
useRef はレンダリングを起こさずに値の保持ができます。
:::

```diff_jsx
const handleStart = () => {
  intervalRef.current = setInterval(() => {
    setTime((prevTime) => prevTime + 10);
  }, 10);
};
```
ストップウォッチを開始するために使用します。
setInterval 関数を使用して、ストップウォッチを更新するためのインターバルを設定します。
ここでは、10ミリ秒ごとに setTime を使用して time を更新します。

```diff_jsx
const handlePause = () => {
  clearInterval(intervalRef.current);
};
```
ストップウォッチを一時停止するために使用します。
clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。

```diff_jsx
const handleReset = () => {
  clearInterval(intervalRef.current);
  setTime(0);
};
```
ストップウォッチをリセットするために使用します。
まず、clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。
次に、setTime を使用して `time` を 0 に設定します。


```diff_jsx
useEffect(() => {
  return () => {
    clearInterval(intervalRef.current);
  };
}, []);
```
useEffect フックを使用して、コンポーネントのアンマウント時にクリーンアップを行います。
ここでは clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。

:::note info
**ポイント**
クリーンアップを行わないと、コンポーネントのアンマウント後にも setInterval 関数が止まらず動き続けてしまいます。
バグに繋がったり、メモリに多大な負荷を与えてしまうので、クリーンアップ関数はめっちゃ大事です！
:::


```diff_jsx
<TimerButton label="Start" onClick={handleStart} />
<TimerButton label="Pause" onClick={handlePause} />
<TimerButton label="Reset" onClick={handleReset} />
```
各ボタンの onClick 属性に上記で作成したハンドラーを設定しています。
これにより開始/一時停止／リセットボタンが機能するようになります。

### ✅ボタンの活性／非活性の制御をする
ボタンに以下のような制御を入れてみます。
- ストップウォッチ停止中
    - Start：活性
    - Pause：非活性
    - Reset：活性
- ストップウォッチ実行中
    - Start：非活性
    - Pause：活性
    - Reset：非活性

App.js を以下のように修正して下さい。

```diff_jsx
export default function App() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);
+ const [isRunning, setIsRunning] = useState(false);

  const handleStart = () => {
    intervalRef.current = setInterval(() => {
      setTime((prevTime) => prevTime + 10);
    }, 10);
+   setIsRunning(true);
  };

  const handlePause = () => {
    clearInterval(intervalRef.current);
+   setIsRunning(false);
  };

  const handleReset = () => {
    clearInterval(intervalRef.current);
    setTime(0);
+   setIsRunning(false);
  };

  useEffect(() => {
    return () => {
      clearInterval(intervalRef.current);
+     setIsRunning(false);
    };
  }, []);
  
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return (
    <div className="main-wrapper">
      <div className="timer-label">
        {hours}:{minutes}:{seconds}:{milliseconds}
      </div>
      <div className="timer-button-container">
        <TimerButton
          label="Start"
          onClick={handleStart}
+         disabled={isRunning}
        />
        <TimerButton
          label="Pause"
          onClick={handlePause}
+         disabled={!isRunning}
        />
        <TimerButton
          label="Reset"
          onClick={handleReset}
+         disabled={isRunning}
        />
      </div>
    </div>
  );
}

function TimerButton(props) {
+ const { label, onClick, disabled } = props;
  return (
+   <button className="timer-button" onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}
```

styles.css に以下を追加して下さい。

```css
button:disabled {
  opacity: 0.5;
  pointer-events: none;
}
```

ボタンの活性／非活性が確認できれば OK

#### 📝解説

```diff_jsx
function TimerButton(props) {
  const { label, onClick, disabled } = props;
  return (
    <button className="timer-button" onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}
```
button タグに disabled（ボタンの活性／非活性の制御）を設定するように変更しています。


```diff_jsx
const [isRunning, setIsRunning] = useState(false);
```
useState フックを使用して、`isRunning` という状態を管理しています。
`isRunning` はストップウォッチが実行中かどうかを表します。
`setIsRunning` はその状態を更新するための関数を返します。
用途としては `TimerButton` コンポーネントの `disabled` の制御に使用します。

```diff_jsx
const handleStart = () => {
  intervalRef.current = setInterval(() => {
    setTime((prevTime) => prevTime + 10);
  }, 10);
  setIsRunning(true);
};

const handlePause = () => {
  clearInterval(intervalRef.current);
  setIsRunning(false);
};

const handleReset = () => {
  clearInterval(intervalRef.current);
  setTime(0);
  setIsRunning(false);
};
```
各ボタン押下時に `setIsRunning` で `isRunning` の状態を更新しています。
制御内容は以下になります。
- Start押下時：ストップウォッチ**実行中**に変更
- Pause押下時：ストップウォッチ**停止中**に変更
- Reset押下時：ストップウォッチ**停止中**に変更

```diff_jsx
<TimerButton
  label="Start"
  onClick={handleStart}
  disabled={isRunning}
/>
<TimerButton
  label="Pause"
  onClick={handlePause}
  disabled={!isRunning}
/>
<TimerButton
  label="Reset"
  onClick={handleReset}
  disabled={isRunning}
/>
```

`disabled={isRunning}` の部分でボタン毎に以下のような制御をしています。
- ストップウォッチ停止中
    - Start：活性
    - Pause：非活性
    - Reset：活性
- ストップウォッチ実行中
    - Start：非活性
    - Pause：活性
    - Reset：非活性

## 3. カスタムHook を作成してみる
各種関数・変数を定義している部分がゴチャゴチャしてきたので、せっかくなのでこの部分をカスタム Hook に切り出してスッキリさせてみましょう。

App.js に以下のカスタムHookを追加してください。

```diff_jsx
function useTimer() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);
  const [isRunning, setIsRunning] = useState(false);

  const handleStart = () => {
    intervalRef.current = setInterval(() => {
      setTime((prevTime) => prevTime + 10);
    }, 10);
    setIsRunning(true);
  };

  const handlePause = () => {
    clearInterval(intervalRef.current);
    setIsRunning(false);
  };

  const handleReset = () => {
    clearInterval(intervalRef.current);
    setTime(0);
    setIsRunning(false);
  };
    
  useEffect(() => {
    return () => {
      clearInterval(intervalRef.current);
      setIsRunning(false);
    };
  }, []);
    
  const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
  const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
  const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
  const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

  return {
    time: `${hours}:${minutes}:${seconds}:${milliseconds}`,
    isRunning,
    handlers: {
      handleStart,
      handlePause,
      handleReset,
    },
  };
}
```


App.js を以下のように修正して下さい。

```diff_jsx
export default function App() {
- const [time, setTime] = useState(0);
- const intervalRef = useRef(null);
- const [isRunning, setIsRunning] = useState(false);

- const handleStart = () => {
-   intervalRef.current = setInterval(() => {
-     setTime((prevTime) => prevTime + 10);
-   }, 10);
-   setIsRunning(true);
-  };

- const handlePause = () => {
-    clearInterval(intervalRef.current);
-   setIsRunning(false);
- };

- const handleReset = () => {
-   clearInterval(intervalRef.current);
-   setTime(0);
-   setIsRunning(false);
- };

- const milliseconds = `0${(time % 1000) / 10}`.slice(-2);
- const seconds = `0${Math.floor(time / 1000) % 60}`.slice(-2);
- const minutes = `0${Math.floor(time / 60000) % 60}`.slice(-2);
- const hours = `0${Math.floor(time / 3600000)}`.slice(-2);

+ const { time, isRunning, handlers } = useTimer();
+ const { handleStart, handlePause, handleReset } = handlers;

  return (
    <div className="main-wrapper">
-     <div className="timer-label">
-       {hours}:{minutes}:{seconds}:{milliseconds}
-     </div>
+     <div className="timer-label">{time}</div>
      <div className="timer-button-container">
        <TimerButton
          label="Start"
          onClick={handleStart}
          disabled={isRunning}
        />
        <TimerButton
          label="Pause"
          onClick={handlePause}
          disabled={!isRunning}
        />
        <TimerButton
          label="Reset"
          onClick={handleReset}
          disabled={isRunning}
        />
      </div>
    </div>
  );
}
```

#### 📝解説

```diff_jsx
function useTimer() {
  /* 中略 */
  return {
    time: `${hours}:${minutes}:${seconds}:${milliseconds}`,
    isRunning,
    handlers: {
      handleStart,
      handlePause,
      handleReset,
    },
  };
}
```

元々 App コンポーネントで実装していたロジックは useTimer フック内で行い、App コンポーネントに必要なデータやハンドラーをオブジェクトで返却しています。

```diff_jsx
export default function App() {
  const { time, isRunning, handlers } = useTimer();
  const { handleStart, handlePause, handleReset } = handlers;
  /* 中略 */
}
```
useTimer フックの呼び出しはこの部分で行ってます。
戻り値は分割代入で取り出して定義しています。

以上！

## 終わりに
お疲れ様でした！
少しでも皆さんの React アレルギーが緩和されたなら嬉しいです笑
今回のハンズオンで実施した内容は現場で必須級の技術になります。
ぜひとも繰り返し復習をして React マスターへの道を目指してみてください！


## 参考文献
- [【React基礎】propsとは？親子コンポーネント受け渡し方をわかりやすく解説！](https://explore-zine.com/react-props/)
- [【備忘録】React Hooksの使い方まとめ](https://zenn.dev/snow_swallow/articles/2dd24c7955dc00)
- [【React】カスタムフック(Custom Hook)を「完全に理解」してみる](https://qiita.com/mamimami0709/items/603c6ea9f9bfa68461f9)

## 初心者向けのサポートをやってます！
MENTA にて HTML/CSS、JavaScript のサポートを行っております。
このソースが上手く動かない、この記事の内容が良く分からないなど何でも良いのでお気軽にご相談ください！
詳細は下記ページよりご参照ください。

https://menta.work/plan/16027?ref=mentor_profile