ReactとMaterial-UIのTIPS集

# この記事について
ここ最近[React](https://ja.reactjs.org/) + [MUI](https://ja.reactjs.org/)でフロントエンド開発をしており、自分の中でベストプラクティス的なものができつつあるので一旦まとめようと思い書いた記事
一部調査中につきTODOの状態(調査完了したら追記する予定)

# React関連
## コンポーネント
### 関数コンポーネントを使おう
Reactのコンポーネントは`関数コンポーネント`と`クラスコンポーネント`の2つに大別される
※[公式ドキュメントから引用](https://ja.reactjs.org/docs/components-and-props.html#function-and-class-components)

```tsx
// 関数コンポーネント👍
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// クラスコンポーネント
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
```

どちらも全く等価なのでどう書くかは好みの問題になるが、個人的には**記述量の少ない関数コンポーネントの方が良い**と思う
また、関数コンポーネントはさらに以下の2通りの記法があり

```tsx
import React from "react";

type Props = {
  name: string;
}

// 関数式👍
function Welcome(props: Props) {
  return <h1>Hello, {props.name}</h1>;
}

// アロー関数
const Welcome: React.FC<Props> = (props: Props) => {
  return <h1>Hello, {props.name}</h1>;
};
```

TypeScriptで型定義をする場合はより記述量の少ない`関数式(function)`の方が望ましいと思う
※ぶっちゃけ関数コンポーネントは戻り値が`JSX.Element`であればいいのでアロー関数でわざわざ`React.FC`で型付けする必要もそんなにないし、その場合記述量の差はほぼないので好みで決めていいと思う

### AtomicDesignに従って階層を分けよう
ReactはUIの部品を`コンポーネント`という単位で宣言して再利用可能にするのがウリ(と自分は思っている)なので、それを活かるようなディレクトリ構成にしましょうという話
AtomicDesignそのものについてはわかりやすい記事がたくさんあるので適当に見繕って読んでいただくとして、以下ではAtomicDesignをReactでのコンポーネント開発のケースにあてはめて解説する

#### Atoms
AtomicDesignの最小単位
AtomicDeisgnの原典では以下のように定義されている

>These atoms include basic HTML elements like form labels, inputs, buttons, and others that can’t be broken down any further without ceasing to be functional.

つまり、`<input>`や`<button>`などのHTMLタグをそのままAtomsのコンポーネントとして実装すればOK
また、そうでなくても(複数のタグを組み合わせて実装しても)それ以上分解できないほどそのコンポーネントの役割が抽象的なのであればAtomsコンポーネントとして実装することになる

https://atomicdesign.bradfrost.com/chapter-2/#atoms

なお、Atomsの実装で重要なのは**widthやpaddingなどを決め打ちしないこと**と、**それらを外部から注入できる形にする(=propsで受け取れるようにする)こと**
Atomsのコンポーネントはあちこちで使いまわすことになるので、サイズを決め打ちしてしまうと`SmallButton`とか`NormalButton`みたいなサイズだけ違うボタンが乱立したりCSSで強引にサイズを変更したりとカオスなコードになってしまう

一方でなるべくデザインに統一感を持たせるためdefaultValueとしてwidthやpaddingを設定するのは大変有効で、他にも`small=200px`, `normal=300px`...のようなenum的な値を作っておくのも良いと思う

#### Molecules
Atomsを組み合わせて実装するコンポーネント
やや大きめの粒度のコンポーネントを実装する際それがMoleculesなのかOrganismsなのかよく悩んでいたが、自分は以下のようなフローチャートにのっとって決定するようにしている

```
単体のHTMLタグそのものか、そのコンポーネントの役割が極めて抽象的 ─YES→ Atoms
 │
 NO
 ↓
そのコンポーネントの役割がビジネスロジックと結びつくもの、具体的なリソースと直接関わるものである ─YES→ Organisms
 │
 NO
 ↓
Molecules
```

具体例を挙げると(Atomsは自明なのでスキップ)以下の通り

- Molecules
  - 検索フォーム
    - 役割は`検索したい文字列を入力し、検索機能を発火すること`であって、具体的に何を検索するかは定められていない(ビジネスロジックと結びついていない)のでMolecules
  - 設定ボタン
    - こういうやつ→⚙️
    - これも何らかの設定画面ないしダイアログを開く役割を持つものの、具体的に何の設定かは定められていないのでMolecules
- Organisms
  - ユーザー検索フォーム
    - 役割が`ユーザーの一覧取得APIをリクエスト`することで、ビジネスロジックと結びついているのでOrganisms
  - ユーザー設定ボタン
    - 役割が`ユーザーの設定変更画面を開くこと`なのでOrganisms

#### Organisms
AtomsやMoleculesを組み合わせて実装する
Organisms内の各コンポーネントのスタイリングはしてもいいがOrganisms自体のサイズはここで決め打ちしてはいけない

#### Templates
Pagesから呼び出されるコンポーネントで、渡されたコンポーネント(基本Organismsのみ)をどのように配置するかについてのみ責務を負う
→スタイリングはするけどロジックはここでは持たない

#### Pages
TemplatesへOrganismsを渡す
また、自身の子コンポーネント(=Organisms)へ渡すべきstatesや関数などはここで宣言する

### 各コンポーネントの依存関係と役割のまとめ
図にまとめると以下のようになる
![dataflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/2f91f0fd-7d41-55ac-43df-3c0f2b19b507.png)

- Pages
  - Organismsを呼び出しTemplatesに渡す
  - 基本的にMolecules、Atomsには関心を持たない
- Templates
  - Pagesから受け取ったコンポーネント(=Organisms)を表示するだけ
  - 各コンポーネントの大きさや配置などはここで決定する
- Organisms
  - Atoms、Molecules、必要なら他のOrganismsを呼び出して1つのコンポーネントを作る
  - 基本的に責務は一つでドメインと結びつくもの
    - チャットを送信するフォーム、商品一覧のページング、プロフィール画面へ遷移するボタンなど
- Molecules
  - Atomsを組み合わせて実装する
  - Organismsと異なりドメインとは直接結びつかない抽象的なコンポーネント
    - テキストボックスを複数持つ何らかの入力フォーム、ページ番号のstateを持つ汎用的なページングなど
- Atoms
  - HTMLのタグと1対1で対応するコンポーネント

## カスタムフックの実装
### カスタムフックでviewとロジックを分離しよう
例えば以下のようなログインフォームがあった場合

```tsx
import React from "react";

export function LoginForm() {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        AuthAPI.login(email, password)
      }}
    >
      <input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
      <input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
      <button type="submit">LOGIN</button>
    </form>
  );
}
```

最低限のロジックしかなくCSSすら書いていないのに結構煩雑で読むのが面倒くさい
また、UTを書く際にもviewとロジックを1ファイルにまとめて書くことになるのでメンテナンスが大変

なので、以下のように**あるコンポーネントで使用したい変数、関数をまとめて返すようなカスタムフック**を実装してviewとロジックを分離しよう

```useLoginForm.ts
import React from "react";
import { LoginFormProps } from "./LoginForm.tsx";

export function useLoginForm(): LoginFormProps {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const onChangeEmail = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) =>
      setEmail(event.target.value),
    []
  );
  const onChangePassword = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) =>
      setPassword(event.target.value),
    []
  );
  const onSubmit = React.useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      AuthAPI.login(email, password);
    },
    [email, password]
  );

  return {
    email,
    onChangeEmail,
    password,
    onChangePassword,
    onSubmit,
  };
}
```

```LoginForm.tsx
import React from "react";

export type LoginFormProps = {
  email: string;
  onChangeEmail: (event: React.ChangeEvent<HTMLInputElement>) => void;
  password: string;
  onChangePassword: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
};

export function LoginForm(props: LoginFormProps) {
  return (
    <form onSubmit={(event) => props.onSubmit(event)}>
      <input
        type="email"
        value={props.email}
        onChange={(event) => props.onChangeEmail(event)}
      />
      <input
        type="password"
        value={props.password}
        onChange={(event) => props.onChangePassword(event)}
      />
      <button type="submit">LOGIN</button>
    </form>
  );
}
```

### 色んなページから参照したい値はContextAPIで管理しよう

例えばAPI実行中に二重でサブミットするのを防ぐためボタンをdisabledにするためのフラグや、認証したユーザーのstateなど
このような値は以下のようにContextAPIを使って管理しよう

```LoadingProvider.tsx
import React from "react";

type LoadingContext = {
  setLoading: React.Dispatch<React.SetStateAction<boolean>>;
};

export const loadingContext = React.createContext<LoadingContext>({
  setLoading: () => null,
});

export function LoadingProvider(props: React.PropsWithChildren<{}>) {
  const [loading, setLoading] = React.useState(false);
  return (
    <loadingContext.Provider
      value={{
        setLoading,
      }}
    >
      {props.children}
    </loadingContext.Provider>
  );
}
```

### コンポーネント毎のカスタムフックの役割のまとめ
![dataflow (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/bc95b9a2-de5f-4f8d-290f-36104230213f.png)

- Providers
  - 各Pagesで使用したい変数/関数はここで生成する
- Pages
  - このページ内の各コンポーネントで使用したい変数/関数はここで生成する
    - 商品一覧ページなら商品の配列などが該当する
    - `ItemTable.tsx`なら配列そのものを使用するし、`ItemPagination.tsx`なら配列の長さによってページ番号を動的に決定することになる
- Organisms
  - そのコンポーネントで必要な変数や関数を生成する
  - 商品一覧テーブルならクリックした商品の詳細ページへ遷移させるとか
- Molecules
  - Organismsと同様
- Atoms
  - Organismsと同様

# Material-UI関連
非デザイナーの自分はCSSのアセットとしてMaterial-UI(MUI)をよく使用しているので、それによって得た知見をまとめる
## デザイン
### ThemeProviderを活用しよう
#### defaultProps
MUIのボタンは以下の3種類の見た目が予め用意されている
![スクリーンショット 2022-05-19 101854.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/1c0ff039-9384-ac92-b2cd-6a3773adde5f.png)
これらを切り替えたい場合は`variant`propsを変えてやればOK

```tsx
<Button variant="text">Text</Button>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
```

https://mui.com/material-ui/react-button/#basic-button

ただし、このアプリでは`contained`で統一したい、このアプリは基本`outlined`を使いたいといった場合に、毎回`variant="contained"`と書くのは面倒だしチーム開発の場合は違うvariantを指定してしまう可能性もある

この問題を解決するためにはMUIのButtonを継承しvariantのデフォルト値を設定したAtomsコンポーネントを実装する手段もあるが、より手っ取り早い方法として**ThemeProvider**の**defaultProps**を活用するのがおすすめ

```ThemeProvider.tsx
import React from "react";
import { createTheme, ThemeProvider as MuiThemeProvider } from "@mui/material";

const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: {
        variant: "contained",
      },
    },
  },
});

export function ThemeProvider(props: React.PropsWithChildren<{}>) {
  return <MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>;
}
```

このようなプロバイダーを実装し、アプリのルートコンポーネント(`create-react-app`で作った場合は`index.tsx`)で呼び出すことでpropsのデフォルト値を変更できる

```index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "./providers/ThemeProvider";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>
);
```

#### palette
#### ダークモード対応
#### 独自定義のvariant

TODO

### スタイリング
各コンポーネントの`size`や`color`などのpropsでは対応できないくらいのスタイリングをしたい場合は以下の2つのAPIを使用する
#### styled関数
[styled-components](https://styled-components.com/)をMUI向けにラップした関数

```tsx
import * as React from 'react';
import Slider, { SliderProps } from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';

const SuccessSlider = styled(Slider)<SliderProps>(({ theme }) => ({
  width: 300,
  color: theme.palette.success.main,
  '& .MuiSlider-thumb': {
    '&:hover, &.Mui-focusVisible': {
      boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
    },
    '&.Mui-active': {
      boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
    },
  },
}));

export default function StyledCustomization() {
  return <SuccessSlider defaultValue={30} />;
}
```

https://mui.com/material-ui/customization/how-to-customize/#2-reusable-component

基本的な使い方はstyled-componentsと同様に、カスタマイズしたいコンポーネントにCSSを書き込んでラップする形になる
`@mui/material/styles`の`styled関数`の特徴として、MUIのThemeオブジェクトを参照できるので`ThemeProvider`でカスタマイズした値を利用することができ、エディタのコード補完の恩恵を受けられる

CSSをラップしたコンポーネント内に閉じて再利用することが可能なので、いろいろな場面で使いまわしたい場合は↓のsxよりこちらのstyled関数の方が良い

#### sx props

各コンポーネントの`sx`propsにCSSを渡す方法

```tsx
<Slider
  defaultValue={30}
  sx={{
    width: 300,
    color: 'success.main',
  }}
/>
```

https://mui.com/material-ui/customization/how-to-customize/#1-one-off-customization

styled関数よりも手軽に書けるもののパフォーマンス的には劣る
なのでわざわざラップして再利用しやすくするまでもないCSSを書きたい場合に使用するのが良い