Reactハンズオンラーニングをやってみた

# はじめに
Reactハンズオンラーニングはオライリー・ジャパンが発行しているReactの解説書です。自分はReactを実務で数ヶ月使用した程度なのですが、レベル感としては丁度よく得るものが多くありました。
中でも6章からのアプリケーションを実装しながらReactコンポーネントをリファクタリングしていく解説は、React上級者がどのように考えてコードの品質を高めていくのかを追体験でき、非常に勉強になりました。
本記事ではその過程をTypeScript化しながら実際にハンズオンでやってみた記録になります。要約した記述になりますので少しでも興味が湧いた方は是非お手に取って、自分でやってみることを強く勧めます。

https://www.amazon.co.jp/React%E3%83%8F%E3%83%B3%E3%82%BA%E3%82%AA%E3%83%B3%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0-%E7%AC%AC2%E7%89%88-%E2%80%95Web%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E9%96%8B%E7%99%BA%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9-Alex-Banks/dp/4873119383

# 準備
下記のようなアプリケーションを作っていきます。
<img width="250px" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612354/a720716e-38bf-3417-d729-eed68522a3cb.gif>

まずはプロジェクトを作成してスターを表示するところまでやってみましょう。

```terminal
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`を以下のように書き換えます

```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`ファイルを作成して以下のコードを記述します。

```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`を以下のように書き換えます。

```tsx
import StartRating from "./StarRating";

export default function App() {
  return <StartRating />;
}
```

`yarn start`して表示を確認してみましょう。
以下のように表示されていたら成功です。
![スクリーンショット 2021-10-17 13.33.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612354/ad79f4d6-db9c-fa55-38fb-84e758e5fffd.png)


:::note alert
`Cannot use JSX unless the '--jsx' flag is provided`
VSCode上で上記のようなエラーが発生した場合、下記記事等を参考にしてVSCodeのTypeScriptのバージョンを変更してみてください
https://qiita.com/tanaShoe/items/3a2f00f9f8906cb55101
:::


しかし現在はスターをSVGで表示しているに過ぎないので、動的に表示を変化させることができません。次章からステートを管理して表示を動的に変更できるようにしていきます。

# useStateを使ってクリックイベントに対応する
以下のように選択中の星は赤色に、選択していない星はグレーにする関数を記述します。

```tsx
const Star = ({ selected = false }) => (
  <FaStar color={selected ? "red" : "gray"} />
)
```

レーティングは５段階評価が多い印象ですが、今回は下記のような処理を追加し星の数を可変にします。これにより任意の段階でレーティング評価できるコンポーネントを作成します。

```tsx
export default function StarRating({ totalStarts = 5 }) {
  retrun [...Array(totalStarts)].map((_, i) => <Star key={i} /> );
}
```

useStateを使ってコンポーネントにステートを追加します。最終的な`StarRating.tsx`は下記のようになります。

```tsx:src/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`内の初期値によってレーティングのカラーを動的に変化させることができるようになりました。
![](https://storage.googleapis.com/zenn-user-upload/a69464dbcce6df68871e2cae.png)

`useState`の値をユーザーのクリックに応じて変化させることができれば、インタラクティブにスターの色を変化させることができるはずです。`StarRating.tsx`内の`Star`関数を別コンポーネントに切り離し、`onClick`イベントハンドラを設定します。

```tsx:src/Star.tsx
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の値が変化するようにします。

```tsx:src/StarRating.tsx
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コンポーネントが再描画されます。以下のようにスターの色がインタラクティブに変化するようになりました。

<img width="250px" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612354/49d92e0e-b1be-95a4-b5f9-592defbace0a.gif />


# アプリケーション全体のステート管理
レーティングの基本的な機能は追加できました。ここから色情報のリストを管理する「色見本アプリ」を実装し、それぞれの色がレーティングの値を持つようにします。

これを愚直に実装しようとすると、多くのコンポーネントがstateの値を持ってしまうアプリケーションになります。これはあまり望ましい状態では無いようで、複数のコンポーネントがstateを持つと機能追加やデバッグが難しくなるという副作用があるようです。

このような場合アプリケーションのstateを1箇所で管理する方が効率的で、１例として最上位のコンポーネントが全てのstateを管理する方法が挙げられrます。最上位のコンポーネントからstateを子コンポーネントにプロパティ値として渡し、アプリケーション全体にデータを反映させるよう実装していくことでstate管理をシンプルな状態に保ちます。

まずアプリケーションで使用するデータを用意します。

```json:src/color-data.json
[
  {
    "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`から読み出して配下のコンポーネントに渡すようにします。

```tsx:src/App.tsx
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`を作成します。

```tsx: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`コンポーネントを追加して色の情報を表示するようにします。

```tsx:src/Color.tsx
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`を以下のように書き換えます。

```tsx:src/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を持たない関数コンポーネントは**純粋関数**と呼ばれ、同じプロパティ値が渡されれば必ず同じ結果が描画されます。純粋関数はテストがしやすくコードの見通しも良くなるという利点があります。

ここまでの実装でデータを描画した結果は下記になります。

<img width="250px" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612354/aad003b2-2b09-7473-8d4e-983f410a5b29.png>


# ユーザーの操作をコンポーネントツリーの下から上に伝える
しかし現在のままでは`color-data.json`の値を表示しているに過ぎない為、色の情報を評価したり削除したりすることができません。

ユーザーの操作は末端のコンポーネントに対して行われますが、色の情報は最上位の`App`コンポーネントにstateとして保持されています。情報を動的に変化させる為には何らかの方法でユーザーの操作を末端のコンポーネントからルートコンポーネントに伝え、stateの値を変化させる必要があります。

`onRemove`関数と`onRate`関数を追加して、stateの削除/更新ができるようにしていきます。

```tsx:src/Color.tsx
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`も同様に実装していきます。

```tsx:src/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`を以下のように記述します

```tsx:src/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`関数を追加し、ユーザーの操作を親コンポーネントに通知するようにします。

```tsx:src/StarRating.tsx
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>
    </>
  );
}
```

下記のように動作できていれば成功です
<img width="250px" src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/612354/98a63528-6559-349e-298e-d23120a7deef.gif>


# おわりに
Reactハンズオンラーニングではここからさらにアプリケーションを拡張していき、リファクタリングを行ってコードの品質を高めつつ実装を行なっていきます。

非常にわかりやすい解説で初級者から上級まで幅広いエンジニアにおすすめできる解説書だと思います。興味のある方はぜひ手に取ってみてください。