Reactハンズオンラーニングをやってみた
はじめに
Reactハンズオンラーニングはオライリー・ジャパンが発行しているReactの解説書です。自分はReactを実務で数ヶ月使用した程度なのですが、レベル感としては丁度よく得るものが多くありました。
中でも6章からのアプリケーションを実装しながらReactコンポーネントをリファクタリングしていく解説は、React上級者がどのように考えてコードの品質を高めていくのかを追体験でき、非常に勉強になりました。
本記事ではその過程をTypeScript化しながら実際にハンズオンでやってみた記録になります。要約した記述になりますので少しでも興味が湧いた方は是非お手に取って、自分でやってみることを強く勧めます。
準備
下記のようなアプリケーションを作っていきます。
まずはプロジェクトを作成してスターを表示するところまでやってみましょう。
npx create-react-app star-rating --template typescript
cd star-rating
不要なファイルを削除します。削除後のディレクトリ構成は下記になります
star-rating
├── README.md
├── node_modules
├── package.json
├── public
│ └ index.html
├── src
│ ├── App.tsx
│ └── index.tsx
└── yarn.lock
src/index.tsx
を以下のように書き換えます
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
星のアイコンを表示する為にreact-icons
パッケージをインストールします。
yarn add react-icons
src
ディレクトリにStarRating.tsx
ファイルを作成して以下のコードを記述します。
import { FaStar } from "react-icons/fa";
export default function StarRating() {
return (
<>
<FaStar color="red" />
<FaStar color="red" />
<FaStar color="red" />
<FaStar color="grey" />
<FaStar color="grey" />
</>
);
}
src/App.tsx
を以下のように書き換えます。
import StartRating from "./StarRating";
export default function App() {
return <StartRating />;
}
yarn start
して表示を確認してみましょう。
以下のように表示されていたら成功です。
しかし現在はスターをSVGで表示しているに過ぎないので、動的に表示を変化させることができません。次章からステートを管理して表示を動的に変更できるようにしていきます。
useStateを使ってクリックイベントに対応する
以下のように選択中の星は赤色に、選択していない星はグレーにする関数を記述します。
const Star = ({ selected = false }) => (
<FaStar color={selected ? "red" : "gray"} />
)
レーティングは5段階評価が多い印象ですが、今回は下記のような処理を追加し星の数を可変にします。これにより任意の段階でレーティング評価できるコンポーネントを作成します。
export default function StarRating({ totalStarts = 5 }) {
retrun [...Array(totalStarts)].map((_, i) => <Star key={i} /> );
}
useStateを使ってコンポーネントにステートを追加します。最終的なStarRating.tsx
は下記のようになります。
import { useState } from "react";
import { FaStar } from "react-icons/fa";
const Star = ({ selected = false }) => (
<FaStar color={selected ? "red" : "gray"} />
);
export default function StarRating({ totalStars = 5 }) {
const [selectedStars] = useState(3);
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star key={i} selected={selectedStars > i} />
))}
<p>
{selectedStars} of {totalStars} stars
</p>
</>
);
}
これでuseState
内の初期値によってレーティングのカラーを動的に変化させることができるようになりました。
useState
の値をユーザーのクリックに応じて変化させることができれば、インタラクティブにスターの色を変化させることができるはずです。StarRating.tsx
内のStar
関数を別コンポーネントに切り離し、onClick
イベントハンドラを設定します。
import { FaStar } from "react-icons/fa";
const Star = ({ selected = false, onSelect = () => {} }) => (
<FaStar color={selected ? "red" : "gray"} onClick={onSelect} />
);
export default Star;
StarRating.tsx
を以下のように書き換え、Starコンポーネントがクリックされるたびにstateの値が変化するようにします。
import { useState } from "react";
import Star from "./Star";
export default function StarRating({ totalStars = 5 }) {
const [selectedStars, setSelectedStars] = useState(3);
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
onSelect={() => setSelectedStars(i + 1)}
/>
))}
<p>
{selectedStars} of {totalStars} stars
</p>
</>
);
}
これによりクリックされたスターに応じてstateの値が変更され、StarRatingコンポーネントが再描画されます。以下のようにスターの色がインタラクティブに変化するようになりました。
アプリケーション全体のステート管理
レーティングの基本的な機能は追加できました。ここから色情報のリストを管理する「色見本アプリ」を実装し、それぞれの色がレーティングの値を持つようにします。
これを愚直に実装しようとすると、多くのコンポーネントがstateの値を持ってしまうアプリケーションになります。これはあまり望ましい状態では無いようで、複数のコンポーネントがstateを持つと機能追加やデバッグが難しくなるという副作用があるようです。
このような場合アプリケーションのstateを1箇所で管理する方が効率的で、1例として最上位のコンポーネントが全てのstateを管理する方法が挙げられます。最上位のコンポーネントからstateを子コンポーネントにプロパティ値として渡し、アプリケーション全体にデータを反映させるよう実装していくことでstate管理をシンプルな状態に保ちます。
まずアプリケーションで使用するデータを用意します。
[
{
"id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
"title": "ocean at dusk",
"color": "#00c4e2",
"rating": 5
},
{
"id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
"title": "lawn",
"color": "#26ac56",
"rating": 3
},
{
"id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
"title": "bright red",
"color": "#ff0000",
"rating": 0
}
]
今回のアプリではルートコンポーネントであるApp
で全てのstateを管理するようにします。App
以外のコンポーネントはstateを持たない為、useState
が使われるのもApp
コンポーネントのみになります。
以下のようにApp.tsx
を書き換えてstate値をcolor-data.json
から読み出して配下のコンポーネントに渡すようにします。
import { useState } from "react";
import colorData from "./color-data.json";
import ColorList from "./ColorList.tsx";
export default function App() {
const [colors] = useState(colorData);
return <ColorList colors={colors} />;
}
src
配下にColorList.tsx
を作成します。
import Color from "./Color";
type ColorProps = {
id: string;
title: string;
color: string;
rating: number;
};
export default function ColorList({ colors = [] }: { colors: ColorProps[] }) {
if (!colors.length) return <div>No Colors Listed.</div>;
return (
<div>
{colors.map((color) => (
<Color key={color.id} {...color} />
))}
</div>
);
}
このコンポーネントは親コンポーネントであるApp.tsx
から渡されたプロパティからcolors
配列を取り出しています。配列の長さが0の場合、ユーザーにメッセージを表示するようにしています。
配列がデータを含んでいる場合はArray.map
でColor
コンポーネントに色の情報を1つずつ渡すようにしています。Color
コンポーネントを追加して色の情報を表示するようにします。
import StarRating from "./StarRating";
export default function Color({
title,
color,
rating,
}: {
title: string;
color: string;
rating: number;
}) {
return (
<section>
<h1>{title}</h1>
<div style={{ height: 50, backgroundColor: color }} />
<StarRating selectedStars={rating} />
</section>
);
}
Color
コンポーネントからrating
のデータをStarRating.tsx
にプロパティ値として渡します。StarRating.tsx
を以下のように書き換えます。
import Star from "./Star";
export default function StarRating({ totalStars = 5, selectedStars = 0 }) {
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star key={i} selected={selectedStars > i} />
))}
<p>
{selectedStars} of {totalStars} stars
</p>
</>
);
}
特筆すべきは前に実装したStarRating.tsx
と比較して今回はコンポーネントがstateの値を持たなくなったということです。stateを持たない関数コンポーネントは純粋関数と呼ばれ、同じプロパティ値が渡されれば必ず同じ結果が描画されます。純粋関数はテストがしやすくコードの見通しも良くなるという利点があります。
ここまでの実装でデータを描画した結果は下記になります。
ユーザーの操作をコンポーネントツリーの下から上に伝える
しかし現在のままではcolor-data.json
の値を表示しているに過ぎない為、色の情報を評価したり削除したりすることができません。
ユーザーの操作は末端のコンポーネントに対して行われますが、色の情報は最上位のApp
コンポーネントにstateとして保持されています。情報を動的に変化させる為には何らかの方法でユーザーの操作を末端のコンポーネントからルートコンポーネントに伝え、stateの値を変化させる必要があります。
onRemove
関数とonRate
関数を追加して、stateの削除/更新ができるようにしていきます。
import StarRating from "./StarRating";
import { FaTrash } from "react-icons/fa";
export default function Color({
id,
title,
color,
rating,
onRemove = () => {},
onRate = () => {},
}: {
id: string;
title: string;
color: string;
rating: number;
onRemove: (id: string) => void;
onRate: (id: string, rating: number) => void;
}) {
return (
<section>
<div style={{ display: "flex", alignItems: "center" }}>
<h1>{title}</h1>
<button
style={{ height: 36, marginLeft: 10 }}
onClick={() => onRemove(id)}
>
<FaTrash />
</button>
</div>
<div style={{ height: 50, backgroundColor: color }} />
<StarRating
selectedStars={rating}
onRate={(rating: number) => onRate(id, rating)}
/>
</section>
);
}
<button>
要素を追加してonClick
イベントハンドラが設定されています。ボタンがクリックされるとonRemove
関数が発火しますがColorコンポーネント自体は処理内容に一切感知しません。親コンポーネントにイベントを通知するだけで、コンポーネントを純粋関数のままに保てています。
ColorList.tsx
も同様に実装していきます。
import Color from "./Color";
type ColorProps = {
id: string;
title: string;
color: string;
rating: number;
};
export default function ColorList({
colors = [],
onRemoveColor = () => {},
onRateColor = () => {}
}: {
colors: ColorProps[],
onRemoveColor: (id: string) => void,
onRateColor: (id: string, rating: number) => void,
}) {
if (!colors.length) return <div>No Colors Listed. (Add a Color)</div>;
return (
<div>
{colors.map(color => (
<Color
key={color.id}
{...color}
onRemove={onRemoveColor}
onRate={onRateColor}
/>
))}
</div>
);
}
最後にApp.tsx
を以下のように記述します
import { useState } from "react";
import colorData from "./color-data.json";
import ColorList from "./ColorList";
export default function App() {
const [colors, setColors] = useState(colorData);
return (
<ColorList
colors={colors}
onRateColor={(id, rating) => {
const newColors = colors.map((color) =>
color.id === id ? { ...color, rating } : color
);
setColors(newColors);
}}
onRemoveColor={(id) => {
const newColors = colors.filter((color) => color.id !== id);
setColors(newColors);
}}
/>
);
}
onRateColor``onRemoveColor
で子コンポーネントから通知を受け取り、stateの値を変化させる処理を行います。結果的にstateをルートコンポーネント一箇所のみで管理したまま、アプリケーションの実装ができています。
StarRating.tsx
にonRate
関数を追加し、ユーザーの操作を親コンポーネントに通知するようにします。
import Star from "./Star";
export default function StarRating({
totalStars = 5,
selectedStars = 0,
onRate = () => {},
}: {
totalStars?: number;
selectedStars: number;
onRate: (num: number) => void;
}) {
return (
<>
{[...Array(totalStars)].map((n, i) => (
<Star
key={i}
selected={selectedStars > i}
onSelect={() => onRate(i + 1)}
/>
))}
<p>
{selectedStars} of {totalStars} stars
</p>
</>
);
}
下記のように動作できていれば成功です
おわりに
Reactハンズオンラーニングではここからさらにアプリケーションを拡張していき、リファクタリングを行ってコードの品質を高めつつ実装を行なっていきます。
非常にわかりやすい解説で初級者から上級まで幅広いエンジニアにおすすめできる解説書だと思います。興味のある方はぜひ手に取ってみてください。
Discussion