【React】よく使うhooks・Component集

おはこんばんちは、[@ちーず](https://qiita.com/cheez921)です。
アドベントカレンダーもやっとこさ1週間ですね！！

本日のテーマは、「よく使うhooks・Component集」です！！
ライブラリ的なものをひたすら紹介するだけの記事です。


## hooks

### react-use
便利なhooks寄せ集め、みたいなもの。

https://github.com/streamich/react-use

チュートリアルやコードの例で見る**`useCounter`**や
最初の呼び出しを無視する`useEffect`が実装されている**`useUpdateEffect`**など、
自分でかくとちょっとめんどくさいような、かゆいところに手が届くhooksがたくさん入っています。

### react-hook-form

ありとあらゆるフォーム周りのStateを取り使えるhooks。

https://react-hook-form.com/jp/

フォームって自前で実装すると、結構大変ですよね。
`react-hook-form`では、バリデーションエラーやエラー処理はもちろんのこと、
入力値を検知して出し分けしたり、変更時に処理を走らせたりなど、結構柔軟に表現することができます。

ただ、個人的に、ちょっと難しいです。。

## Component
基本的にUIフレームワークを使わない前提で紹介します！

### Selector - `React Select`

https://react-select.com/home

通常のセレクターだけではなく、
複数選択できるセレクターや、タグ表示にできるセレクターなど、
さまざまなパターンのセレクターが表現できます。

CSSも結構自由自在に書くことができるので、おすすめです。

###  Modal - `react-modal`
Reactのコミュニティが提供してくれているモーダル。
必要最低限且つシンプルな機能が兼ね備えられているため、中央に表示するモーダル
スタイルもオブジェクトスタイルで拡張可能。
(中央以外のレイアウトはちょっとめんどくさかった)

https://github.com/reactjs/react-modal

### Tab - `react-tabs`
Reactのコミュニティが提供してくれているタブ。
モーダル同様、必要最低限且つシンプルな機能が兼ね備えられている。
ちゃんと`role`の指定もできる

https://github.com/reactjs/react-tabs

### Transition - `react-transition-group`
またまたReactのコミュニティが提供してくれているアニメーションを簡単に実装できるComponent。

https://github.com/reactjs/react-transition-group


### Accordion
正直あまり良いプラグインはなかったので自前で実装しました。
Stateによってheaderのスタイルが変わる(`+` → `-`みたいな)ことを実現したかったため、
headerとbodyに開閉状態を配布するプロバイダーで囲っています。
headerやbodyは上から自由にスタイルを拡張できるよう作りました。


<details><summary>サンプルコード</summary><div>

```AccordionProvider.tsx
import { useReducer, createContext, DispatchWithoutAction, FC } from 'react';

export type AccordionContextType = {
  isOpen: boolean;
  toggleAccordion: DispatchWithoutAction;
  accordionName: string;
};

export const AccordionContext = createContext<AccordionContextType>({
  isOpen: false,
  toggleAccordion: () => {
    // no-op
  },
  accordionName: ''
});

export type AccordionProviderProps = {
  initialIsOpen: boolean;
};

export const AccordionProvider: FC<AccordionProviderProps> = ({
  children,
  initialIsOpen,
  accordionName
}) => {
  const [isOpen, toggleAccordion] = useReducer(
    (state) => !state,
    initialIsOpen
  );

  return (
    <AccordionContext.Provider value={{ isOpen, toggleAccordion, accordionName }}>
      {children}
    </AccordionContext.Provider>
  );
};

export const useAccordionContext = (): AccordionContextType => {
  return useContext<AccordionContextType>(AccordionContext);
};

```

```AccordionHeader.tsx
import React, { VFC, ReactNode } from 'react';
import { useAccordionContext } from 'AccordionProvider';

type Props = {
  children: ReactNode;
} & JSX.IntrinsicElements['div'];

export const AccordionHeader: VFC<Props> = ({
  children,
  ...attrs
}) => {
  const { isOpen, toggleAccordion, accordionName } = useAccordionContext();

  return (
    <div
      onClick={toggleAccordion}
      aria-controls={accordionName}
      aria-expanded={isOpen}
      {...attrs}
    >
      {children}
    </div>
  );
};
```


```AccordionBody.tsx
import React, { FC, ReactNode } from 'react';
import { useAccordionContext } from 'AccordionProvider';
import { CSSTransition } from 'react-transition-group';

type Props = {
  children: ReactNode;
} & JSX.IntrinsicElements['div'];

export const AccordionHeader: FC<Props> = ({
  children,
  ...attrs
}) => {
  const { isOpen, accordionName } = useAccordionContext();
  // アニメーション周り色々している
  const { bodyElm, transitionMethod } = useAccordionBody();

  return (
    <CSSTransition in={isOpen} timeout={duration} {...transitionMethod}>
      <div
        id={accordionName}
        aria-hidden={!isOpen}
        ref={bodyElm}
        css={styles.container(duration)}
        {...attr}
      >
        {children}
      </div>
    </CSSTransition>
  );
};
```


</div></details>




### Rating Star
これもスター数が多いものがなく、心折れながらもなんとか実装しました。
なかなか強引な方法ですが、svgのグラデーションを用いて実現しています。
(拡張性はあまりないです。)


<details><summary>サンプルコード</summary><div>

``` RatingStars.tsx
import React, { FC } from 'react';
import { RatingStarIcon } from './RatingStarIcon';
import { css } from '@emotion/react';


type Props = {
  size: number;
  score: number;
  id: string;
  activeColor: string;
  disabledColor: string;
  gap?: SizeValues;
  max?: number;
} & JSX.IntrinsicElements['div'];

const styles = {
  container: (gap: number) => css`
    display: flex;

    > * {
      &:not(:last-child) {
        margin-right: ${gap / 10}rem;
      }
    }
  `,
};

export const RatingStars: FC<Props> = ({
  size,
  score,
  gap = '2px',
  id,
  activeColor.,
  disabledColor,
  max = 5,
  ...attrs
}) => {
  const starRate = (count: number, score: number): number => {
    if (score - count > 1) return 100;
    if (score - count > 0) return (score - count) * 100;
    return 0;
  };

  return (
    <div css={styles.container(gap)} {...attrs}>
      {[...Array(max)].map((_, index) => {
        const rate = starRate(index, score);
        return (
          <React.Fragment key={`star-${index}`}>
            <RatingStarIcon
              id={id}
              width={size}
              activeColor={activeColor}
              activeColor={disabledColor}
              height={size}
              rate={rate}
              index={index}
            />
          </React.Fragment>
        );
      })}
    </div>
  );
};
```

```RatingStarIcon.tsx
import React, { FC } from 'react';

type Props = {
  id: string;
  rate: number;
  index: number;
  activeColor: string;
  disabledColor: string;
  width?: number;
  height?: number;
};

export const RatingStarIcon: FC<Props> = ({
  id,
  width = 30,
  height = 30,
  activeColor,
  disabledColor,
  index,
  rate,
}) => {
  return (
    <svg
      width={width}
      height={height}
      viewBox="0 0 30 30"
      xmlns="http://www.w3.org/2000/svg"
    >
      <defs>
        <linearGradient
          id={`gradient${index}_${id}`}
          x1="0"
          x2="1"
          y1="0"
          y2="0"
        >
          <stop offset="0%" stopColor={activeColor} />
          <stop offset={`${rate}%`} stopColor={activeColor} />
          <stop offset={`${rate}%`} stopColor={disabledColor} />
          <stop offset="100%" stopColor={disabledColor} />
        </linearGradient>
      </defs>
      <path
        fill={`url(#gradient${index}_${id})`}
        d="M15 24.1105L24.27 30L21.81 18.9L30 11.4316L19.215 10.4684L15 0L10.785 10.4684L0 11.4316L8.19 18.9L5.73 30L15 24.1105Z"
      />
    </svg>
  );
};

```

</div></details>