ReactとTailwindCSSで5つ星評価のUIを実装する

React,Tailwindを使って、星で評価を投稿するUIを実現する機会があり、
どう実現するか悩み検索するも、なかなか記事が見つからなかったので、私が実際に書いたコードを元に今回記事を書いてみました。


使っている技術は

|  技術  |  バージョン  |
| ---- | ---- |
| React.js | 17.0.38 |
| Tailwind CSS | 3.0.13 |
| TypeScript | 4.5.4 |

です。完成形のイメージは以下のようなものです。

![star 14.56.55.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/c05d26bd-6c6d-5ecc-49fe-600030facf36.gif)

## 星アイコンの実装
まずは星のアイコンを実装します。
ここでは、`selected`がTrueなら黄色、`selected`がFalseならばグレーになるようにします。

![スクリーンショット 2022-01-14 23.49.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/37983bc0-0929-4eb2-3a63-0dec63ea378c.png)　![スクリーンショット 2022-01-14 23.49.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/2051f48e-c812-751a-edc6-cf0171b20016.png)


以下は[ReactIcon](https://react-icons.github.io/react-icons/search?q=aifillstar)というnpmを利用していますが、SVGアイコンで実装してもそれほど変わりません。

```javascript
import React, { VFC } from "react";

import { AiFillStar } from "react-icons/ai";

type PropsType = {
  selected: boolean;
  size?: string;
};

export const ColorizeStarIcon: VFC<PropsType> = ({
  selected,
  size,
}: PropsType) => {
  const color = selected ? "text-yellow-400" : "text-gray-100";
  const hoverColor = selected ? "text-gray-100" : "text-yellow-400";

  return (
    <AiFillStar
      icon="fillStar"
      color={color}
      className={`hover:${hoverColor}`}
      size={size}
    />
  );
};
```

このアイコンのポイントは、

1. テキストの色を 選択済みの場合は黄色、選択されていなければグレーにする
1. Hoverされたときのテキストの色は、選択済みの場合はホバーされるとグレーになる、選択されていなければホバーされると黄色になる

という　この部分です。

```js
  const color = selected ? "text-yellow-400" : "text-gray-100";
  const hoverColor = selected ? "text-gray-100" : "text-yellow-400";
```



## 星を並べる
続いて上で作った星を並べていきます。

![スクリーンショット 2022-01-14 23.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/1f6b15d3-150b-f194-7d73-258710c3a398.png)

- 星の数(count)
- 選択された星の数(value)
- 星を変更できるか(readonly)
- 星の変更時の関数(onChange)

をpropsで渡されるコンポーネントを作成します。

以下、コードをしまします。詳しくは下で解説します。

```js
import React, { VFC, useState } from "react";

import { ColorizeStarIcon } from "~/components/atoms";

type PropsType = {
  count: number;
  value: number;
  readonly?: boolean;
  size?: string;
  onChange?: (value: number) => void;
};

export const StarRatingField: VFC<PropsType> = ({
  count,
  value,
  readonly = false,
  size,
  onChange,
}: PropsType) => {
  const [hover, setHover] = useState(-1); //  Hover時にも色を変更できるように、hoverされている星の数を管理します

  const onClick = (starNumber: number) => {
    if (readonly) return;

    onChange && onChange(starNumber);
    setHover(-1);
  };

  const onHover = (starNumber: number) => {
    if (readonly) return;

    setHover(starNumber);
  };

  const onMouseLeave = () => {
    if (readonly) return;

    setHover(-1);
  };

  const currentValue = hover >= 0 ? hover : value;

  return (
    <>
      <ul className="flex items-center space-x-1">
        {[...Array(count)] // countの数だけの要素を持った配列を作成
          .map((_, i) => i + 1)
          .map((starNumber) => (
            <li
              title={`${starNumber} star`}
              key={starNumber}
              onClick={() => onClick(starNumber)}
              onMouseOver={() => onHover(starNumber)}
              onMouseLeave={() => onMouseLeave()}
              className={readonly ? "" : "cursor-pointer"}
            >
              <ColorizeStarIcon
                selected={starNumber <= currentValue} // currentValue以下のindexは、selectedをTrueにする
                size={size}
              />
            </li>
          ))}
      </ul>
    </>
  );
};
```

ポイントは、

1. 星(count)の数だけ配列を作る: [...Array(count)]
1. 選択された星の数(value)の数だけ、先ほど作った ColorizeStarIconコンポーネントのselectedをTrueにします
1. Hover時にも色を変更できるように、hoverされている星の数を管理します
1. マウスがhoverされたときには、そのhoverされた星のindex+1番にhoverの数を更新します
1. マウスのhoverが外れたとき と クリックされたとき には、そのhoverの数を-1に更新します



ざっくりですが、上記のようにすると、以下のような星の評価をできるUIを実装できます。

![star 14.56.55.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107345/c05d26bd-6c6d-5ecc-49fe-600030facf36.gif)

必要になったときにご参考ください。