ReactのMaterial UI (MUI) を細かくデザインする

Material UI (以下、MUI)は、Reactでアプリケーションを作る際にあらかじめデザインされたコンポーネントを用意しているライブラリです。MUI のいいところは簡単にすぐにデザインされたUIを使える点です。細かいことを気にしなければとりあえず動くものがすぐに作れます。が、その一方で、すでにデザインされているため、デザインや処理を細かく変更する方法を一つ一つ調べる必要があります（全ての言語がそうですが、多くの言語ではなぜ動くのか学んでから動かしますが、MUIではその順番が逆になることがしばしばあります）。ここでは、挙動に関しては多くは触れず、デザインを細かく設定する方法を紹介します。

# 本記事の目的
- Reactアプリケーション内で使える MUI の細かい設定 (主にデザイン) ができるようになる

# 実行環境
ReactのMUIが動けば、本記事はさほど環境に依存しないと思います。が、updateによる変更部分が多い分野のため、versionは必要に応じて確認してください。
- Mac OS M1
- node.js: v16.15.1
- npm: 8.11.0

# 再現方法
MUIを実際にwebで表示しながら挙動を確認していくので、ここではアプリケーションを作成するところから始めます。既に作成したアプリの中で適応したい場合は、適宜読み飛ばしながら確認してください！

## create-react-appでアプリを作成する
```
$ mkdir qiita
$ cd qiita
$ npx create-react-app qiita-app
```

下にディレクトリ構成のscreenshotを添付します。
アプリが正常に作成されました。

![スクリーンショット 2022-11-29 19.36.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/b7cc754e-baa0-49a9-024b-227e65abc58e.png)

```
$ cd qiita-app
$ npm start
```

[http://localhost:3000](http://localhost:3000)でReactの初期画面が表示されました！

![スクリーンショット 2022-11-29 19.40.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/940e0b9d-7350-6fed-d3b2-1dbf6f28a550.png)

必要のないものを削除していきます。

```diff_javascript
// src/App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
+    <>
+    
+    </>
  );
}

export default App;
```
[http://localhost:3000](http://localhost:3000) には真っ白い画面が表示されたこと思います。それではMUIをinstallしていきます。

```
$ npm install @mui/material @emotion/react @emotion/styled
```

本記事では下記のversionを使います。package.json内のdependenciesにてversionを確認してください。

```package.json
{
  ...
  "dependencies": {
    "@emotion/react": "^11.10.5",
    "@emotion/styled": "^11.10.5",
    "@mui/material": "^5.10.16",
    ...
  },
  ...
```

試しにButtonを置きます。

:::note info
MUIのcomponentは必ず大文字で始まります。
:::

```diff_javascript
// src/App.js
import logo from './logo.svg';
import './App.css';

+ import {
+   Button, 
+ } from "@mui/material";

function App() {
  return (
    <>
+      <Button variant="contained">Success to import MUI</Button>
    </>
  );
}

export default App;
```

# 本編
## コンポーネント化したい
MUI で `divタグ` のように要素をコンポーネント化してstyleを当てはめる際によく使われるのが、 `Box` `Grid` `Stack` です。各要素に同じstyleを適用したい際に、それぞれの要素に対してstyleを書くよりも、コンポーネント化した後に親コンポーネントに対してスタイルを適用することで、運用性を高めます。基本的には `Box` を利用し、子要素に対して特別な配置を適用したい場合に `Grid` や `Stack` 等を使います。

### Box

> The Box component serves as a wrapper component for most of the CSS utility needs.

Boxコンポーネントは、CSSユーティリティに必要なほとんどのもののラッパーコンポーネントとして機能します。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import {
  Box, 
  Button, 
} from "@mui/material";

function App() {
  const height = 200;
  return (
    <>
      <Box sx={{ backgroundColor: "#000000", height: 300, width: "auto" }}>
        <Button variant="contained">Button Black</Button>
      </Box>
      <Box sx={{ bgcolor: "#444444", height: 300, width: "50%" }}>
        <Button variant="contained">This is Gray Button</Button>
      </Box>
      <Box sx={{ bgcolor: "#888888", height: height, width: 200 }}>
        <Button variant="contained">This is Another Gray Button</Button>
      </Box>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 3.09.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/e7e5b2c4-3980-1ec5-27ba-905b1f4a9c57.png)

- style を適用する場合は、`sx` に対してCSSの情報を記入します。
- css ではハイフン繋ぎの `チェインケース`、ここでは大文字小文字で繋ぐ `キャメルケース` が用いられます。
    - css では `background-color: "#000000"`だが、ここでは `backgroundColor: "#000000"と表現する
    - `bgcolor` は `backgroundColor` の省略形
- sx内のvalueは `文字列` `数字` `変数` のいずれかで指定します。
    - `100%` や '50rm` などの場合はそれを文字列として表記します。

### Grid

> The Material Design responsive layout grid adapts to screen size and orientation, ensuring consistency across layouts.

マテリアルデザインのレスポンシブレイアウトグリッドは、画面サイズや向きに適応し、レイアウトの一貫性を確保します。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import {
  Button, 
  Grid, 
} from "@mui/material";

function App() {
  const height = 200;
  return (
    <>
    <Grid container>
      <Grid item xs={6} sx={{bgcolor: "#000000", p: 1 }}>
        <Button variant="contained">Button Black</Button>
      </Grid>
      <Grid item xs={6} sx={{ bgcolor: "#444444", p: 1 }}>
        <Button variant="contained">This is Gray Button</Button>
      </Grid>
      <Grid item xs={8} sx={{ bgcolor: "#888888", p: 1 }}>
        <Button variant="contained">This is Another Gray Button</Button>
      </Grid>
    </Grid>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 3.26.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/bde93831-6a43-0789-8c9f-b15f74a201c9.png)

- `Grid` は `Box` に対して `display: "flex"` を適用したようにgridシステムを作成することができます。
- `Grid` には `container` と `item`の二種類があります。
    - `container` は必ず親コンポーネントとして指定する必要があります。
    - `container` はその中身が横方向に12等分されており、`item` でその横幅を指定します。
    - `item` には `xs` を指定することで横幅を設定します。
    - `xs={6}` は `container` の半分、`xs={8}`は`container`の2/3の横幅になります。
- `Grid` は List を map して、Listの中身を順に表示させたい場合に多用されます。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import {
  Button, 
  Grid, 
} from "@mui/material";

const data = [
  "1111", "2222", "3333", "4444", "5555", "6666", "7777", "8888", "9999", "0000",
];

function App() {
  return (
    <>
      <Grid container>
        {data.map((v, key) => {
          return (
            <Grid item key={key} xs={4} sx={{ bgcolor: "#444444" }}>
              <Button variant="contained">{ v }</Button>
            </Grid>
          )
        })}
      </Grid>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 4.38.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/aefccf40-4dfb-7e79-4634-be7e6e8e4153.png)

- gridシステムを用いることで、個数の決まっていない子コンポーネントに対して想定通りの挙動にさせることができます
    - 例えば、Twitterの投稿やブログ記事の投稿などによく使われます。

### Stack
> The Stack component manages layout of immediate children along the vertical or horizontal axis with optional spacing and/or dividers between each child.

Stackコンポーネントは、垂直軸または水平軸に沿った即時の子のレイアウトを管理し、各子の間にオプションで間隔や仕切りを設定することができます。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import {
  Button, 
  Stack, 
} from "@mui/material";

function App() {
  return (
    <>
      <Stack direction="row" >
        <Button variant="counatined" sx={{ bgcolor: "#444444" }}>Right</Button>
        <Button variant="counatined" sx={{ bgcolor: "#888888" }}>Center</Button>
        <Button variant="counatined" sx={{ bgcolor: "#AAAAAA" }}>Left</Button>
      </Stack>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 4.48.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/94df2544-618b-48f7-1e59-ef2bd50160df.png)

- `Stack` は縦方向・横方向の配置に適しています。
- `Box` に対して適切にstyleをすれば再現することができるため、無理やり横方向に設定したい場合には使いやすいです。

## Buttonを押したい
```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import {
  Button, 
} from "@mui/material";

function App() {
  return (
    <>
      <Button
        variant="text"
        sx={{ m: 1 }}
      >text</Button>
      <Button
        variant="contained"
        sx={{ m: 1 }}
      >contained</Button>
      <Button
        variant="outlined"
        sx={{ m: 1 }}
      >outlined</Button>
      <Button
        variant="contained"
        color="inherit"
        sx={{ m: 1 }}
      >inherit</Button>
      <Button
        variant="contained"
        color="error"
        sx={{ m: 1 }}
      >error</Button>
      <Button
        variant="contained"
        color="primary"
        sx={{ m: 1 }}
      >primary</Button>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 4.57.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/453cddfb-8950-9d66-864f-e3f3499ddcdf.png)

- MUI Buttonは`variant`と`color`でそのstyleを簡単に設定できます。
  - variantは、`text | contained | outlined` を取ります。
  - colorは、`inherit | primary | secondary | success | error | info | warning` を取ります。
  - これらに新たにstyleを自作して追加することもできます。が、これをベースにstyleを適宜変更することで、MUIの特徴を生かしつつ細かい設定を施すことができます。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import React from "react";
import {
  Box, 
  Button, 
} from "@mui/material";

function App() {
  const [value, setValue] = React.useState(false);
  const handleClick = () => {
    setValue(!value);
  };
  return (
    <>
      <Box sx={{ bgcolor: "#888888", height: 400, width: "auto", textAlign: "center" }}>
        { String(value) }
        <Button
          fullWidth
          variant="contained"
          color="info"
          onClick={handleClick}
          sx={{
            textDecoration: "none",
            textTransform: "none",
            color: "red",
            mx: 2, my: 1,
            ":hover": {
              textDecoration: "underline",
            },
          }}
        >Please Click Me</Button>
      </Box>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 8.59.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/b0b66294-dfa6-9409-d6af-e5810e2c6163.png)

- マージン (margin)
    - `marigin-top` は `marginTop` か `mt` と表します。
    - `mx` は横方向 (`ml`+`mr`)、`my`は縦方向 (`mt`+`mb`)
- `fullWidth` は親コンポーネントと同じ横幅を取ります。
    - `mx: 2` としているため、右側に飛び出していることがわかります。
    - この場合は、親コンポーネントに対してpaddingを設定した方が、想定通りの挙動になるでしょう。
- Buttonコンポーネントでは、アルファベットが必ず大文字になります
    - `textTransform: none` を指定することで、自動で大文字にする処理を無効にしてくれます。
- hover時の処理は、`":hover": {}`で指定します
- 挙動
    - 画面に表示する変数は通常の変数ではなく、stateで指定することで、中身の変更と同時に必要箇所だけReactが適切にレンダリングしてくれます（便利ですね）。
    - onClick内には基本的にcallback関数を作成することをお勧めします。保守性が高まります。また、`onClick={e => setValue(e.target.value)}` とした場合は、Buttonがクリックされる毎に関数が生成されるため、非効率的です。この程度であれば誤差の範囲内でしょうが、処理が複雑になった時のことを考えて、別で関数を設定しましょう。

## TextField はマスト

> Text Fields let users enter and edit text.

テキストフィールドは、ユーザーがテキストを入力・編集するためのものです。

```src/App.js
// src/App.js
import logo from './logo.svg';
import './App.css';

import React from "react";
import {
  Box, 
  TextField, 
} from "@mui/material";

function App() {
  const [value, setValue] = React.useState("default");
  const [isError, setIsError] = React.useState(false);

  const onChangeValue = (e) => {
    if (e.target.value.length > 8) {
      setIsError(true);
    } else {
      setIsError(false);
    }
    setValue(e.target.value);
  };
  const onChangeValueWrong = (e) => {
    setValue(e.target.value);
    if (value.length > 8) {
      setIsError(true);
    } else {
      setIsError(false);
    }
    
  };

  return (
    <>
      <Box sx={{ bgcolor: "#888888", height: 400, width: 600, px: 1, py: 2, textAlign: "center" }}>
        <TextField 
          fullWidth
          id="value"
          label="value"
          variant="outlined"
          error={isError}
          value={value}
          onChange={onChangeValue}
          helperText="8文字以下で入力してください"
          InputProps={{
            style: {
              fontWeight: "bold",
              fontSize: 24,
            }
          }}
          sx={{
            fontSize: 12,
          }}
        />
        { value }
      </Box>
    </>
  );
}

export default App;
```

![スクリーンショット 2022-12-02 9.43.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/821596/1b2f48ab-fdaf-24a6-b61a-2385ff505050.png)

- よく勘違いするfontに対する処理
    - `sx` に対して `fontSize` を指定しても入力内容 (default) の文字サイズが変わりません。この `fontSize' が指しているのは `label` の文字 (value) サイズです。
    - 入力内容 (default) にstyleを適用したい場合は、`InputProps: {{ style: {} }}` に指定してください。
    - また、`InputProps: {{ inputProps: {} }}` のように `TextField` 内をvalidateすることがあるかと思いますが、この時の小文字や大文字に注意してください。
- validation はざっくり3種類
    - そもそも9文字以上表示したくない
        - useRef や inputProps 等を使ってください
        - ただし、日本語入力を想定する場合にこの処理は適切ではありません。平仮名かな漢字に変換する際に、文字数が減少するため、例え漢字では8文字だったとしても、平仮名で9文字以上の場合に変換する前に入力ができなくなります。
    - 何かしらのButtonがクリックされると同時にvalidateしたい
        - そのボタンのonClickの処理にvalidation処理を加えてください
        - ユーザーの手戻りが発生するため、ユーザーエクスペリエンスを下げるとされています
    - 9文字以上表示するがそれがerrorだと伝えたい
        - 今回のようにしてください
        - ただし、これだけだと内部でerrorだと理解しているだけなので、error時にはButton等をdisabledにするなどの処理をすることを推奨します
- 挙動
    - 変数の中身が変わると同時に表示内容を変えたいので今回もstateを使います。
    - 今回は、9文字以上の入力があると、`TextField` をエラー状態にする処理をしています。
    - バグるやつ
        - `onChange={onChangeValueWrong}` に変更して挙動を確認してみてください。
        - 入力を続けた場合は10文字目で、消した場合は7文字でエラー状態になると思います。
        - 超高速で内容を変更した場合は挙動が変わります。
        - webの難しいところで、プログラミング言語の原則に反して、上から順番に処理が適応されない場合があります。
        - この場合は、`setValue` 自体は先に走っていますが、stateの更新が描画に間に合わず、`value` が更新されるより前に `setIsError` の処理が完了してしまうのです。
        - stateの変更にはラグがあることを念頭に入れる必要があります。

# まとめ
本記事では、Material UI (MUI) のデザインを中心に、`@material-ui/material`の中で最も頻繁に利用する、`Box` `Grid` `Stack` `Button` `TextField` のデザインの仕方と挙動について、実例を紹介しました。
MUIは、何となく動かせる代わりに、なぜ動くのかを理解することをうろそかにしてしまいがちです。挙動に関しても、webが表示される仕組みから先に学ぶ必要があるものを、Reactは自動でいい感じにやってくれちゃいます。
私は、MUIで何となく動くことを確認した後に、それがなぜ動くのかに焦点を移していくことが大事だと考えています。この記事を読み終えた後に、「なぜ」に関する学習にも目を向けてくれれば幸いです。

# 参考文献
- Material UI 公式ドキュメント

https://mui.com/material-ui/

- React の基礎について手を動かしながら理解できます

https://qiita.com/TsutomuNakamura/items/72d8cf9f07a5a30be048