概要
今回はストップウォッチアプリをハンズオン形式で作成します。
特に 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 とは
今回触る useState/useRef/useEffect 以外の Hooks は見なくて OK です。
カスタムHook とは
【React】カスタムフック(Custom Hook)を「完全に理解」してみる
ハンズオンの準備
ハンズオンの環境ですが、WEBエディターで行います。
ですので、面倒な環境構築は一切不要です
- CodeSandbox にアクセスし、サインインする
サインインしないとコードを編集できないので注意です!
-
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 <></>; }
-
styles.css の不要な部分を削除する
- .App { - font-family: sans-serif; - text-align: center; - }
以上!
ハンズオン開始
1. 実際にコンポーネントを作成して Props を渡してみる
こんな感じの画面を作成します。
あくまでガワだけでストップウォッチ機能は次章「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
📝解説
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>
);
}
📝解説
ここは 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>
);
}
📝解説
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)
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(活性/非活性の制御は次章で実施)
📝解説
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);
};
各ボタン押下時に setIsRunning
で isRunning
の状態を更新しています。
制御内容は以下になります。
- 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 マスターへの道を目指してみてください!
参考文献
- 【React基礎】propsとは?親子コンポーネント受け渡し方をわかりやすく解説!
- 【備忘録】React Hooksの使い方まとめ
- 【React】カスタムフック(Custom Hook)を「完全に理解」してみる
初心者向けのサポートをやってます!
MENTA にて HTML/CSS、JavaScript のサポートを行っております。
このソースが上手く動かない、この記事の内容が良く分からないなど何でも良いのでお気軽にご相談ください!
詳細は下記ページよりご参照ください。