2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

概要

upload_bb4023ab6dddedf4fbb78e9a4c4c5132.gif

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

サンプルコード

github でコードを公開しています。
以下のコードを元にハンズオンを進めます。

React Hook とは

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

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

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

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

引用)フック早わかり
引用)5分でわかるReact Hooks

本ハンズオンで学べること

  • コンポーネントを作成して Props を渡してみる
  • Hook を触ってみる(useState/useRef/useEffect)
  • カスタムHook を作成してみる

対象者

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

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

Props とは

【React基礎】propsとは?親子コンポーネント受け渡し方をわかりやすく解説!

useState/useRef/useEffect とは

【備忘録】React Hooksの使い方まとめ

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

カスタムHook とは

【React】カスタムフック(Custom Hook)を「完全に理解」してみる

ハンズオンの準備

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

  1. CodeSandbox にアクセスし、サインインする

サインインしないとコードを編集できないので注意です!

  1. 「Start for free」を押下
    image.png

  2. 「React」押下
    image.png

  3. 「Open template」押下
    image.png

  4. App.js の不要な部分を削除する

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

    - .App {
    -   font-family: sans-serif;
    -   text-align: center;
    - }
    

以上!

ハンズオン開始

1. 実際にコンポーネントを作成して Props を渡してみる

image.png

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

✅まずはラベルとボタンを表示する

App.js を以下のように修正して下さい。
全コピーして貼り付けで OK です。

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

📝解説

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)を、ミリ秒、秒、分、時間の単位に分割します。

  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 に以下を追加して下さい。

.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 を以下のように修正して下さい。

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

📝解説

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

✅ボタンを共通コンポーネント化して Props を渡してみる

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

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

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

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

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

📝解説

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

Props を受け取っているコンポーネントです。
ボタンのラベルとクリックイベントを受け取って、各 Props を設定した button タグを返却しています。

<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
useState、useRef、useEffect を用いて、ストップウォッチ機能を実装してみます。

✅ストップウォッチ機能を作成する

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

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

📝解説

import { useState, useRef, useEffect } from "react";

import 文を使用して、useState/useRef/useEffect のフックをインポートしています。

const [time, setTime] = useState(0);

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

const intervalRef = useRef(null);

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

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

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

ストップウォッチを開始するために使用します。
setInterval 関数を使用して、ストップウォッチを更新するためのインターバルを設定します。
ここでは、10ミリ秒ごとに setTime を使用して time を更新します。

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

ストップウォッチを一時停止するために使用します。
clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。

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

ストップウォッチをリセットするために使用します。
まず、clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。
次に、setTime を使用して time を 0 に設定します。

useEffect(() => {
  return () => {
    clearInterval(intervalRef.current);
  };
}, []);

useEffect フックを使用して、コンポーネントのアンマウント時にクリーンアップを行います。
ここでは clearInterval 関数を使用して、setInterval 関数で設定されたインターバルをクリアします。

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

<TimerButton label="Start" onClick={handleStart} />
<TimerButton label="Pause" onClick={handlePause} />
<TimerButton label="Reset" onClick={handleReset} />

各ボタンの onClick 属性に上記で作成したハンドラーを設定しています。
これにより開始/一時停止/リセットボタンが機能するようになります。

✅ボタンの活性/非活性の制御をする

ボタンに以下のような制御を入れてみます。

  • ストップウォッチ停止中
    • Start:活性
    • Pause:非活性
    • Reset:活性
  • ストップウォッチ実行中
    • Start:非活性
    • Pause:活性
    • Reset:非活性

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

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 に以下を追加して下さい。

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

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

📝解説

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

button タグに disabled(ボタンの活性/非活性の制御)を設定するように変更しています。

const [isRunning, setIsRunning] = useState(false);

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

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);
};

各ボタン押下時に setIsRunningisRunning の状態を更新しています。
制御内容は以下になります。

  • Start押下時:ストップウォッチ実行中に変更
  • Pause押下時:ストップウォッチ停止中に変更
  • Reset押下時:ストップウォッチ停止中に変更
<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を追加してください。

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 を以下のように修正して下さい。

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>
  );
}

📝解説

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

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

export default function App() {
  const { time, isRunning, handlers } = useTimer();
  const { handleStart, handlePause, handleReset } = handlers;
  /* 中略 */
}

useTimer フックの呼び出しはこの部分で行ってます。
戻り値は分割代入で取り出して定義しています。

以上!

終わりに

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

参考文献

初心者向けのサポートをやってます!

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?