Material UI v5  と Emotion の環境構築

以前 `Material UI` と `Emotion` を使った環境構築について記事を書いたのですが、Material UI が **v4 から v5** にアップデートされたので、今回は以前の記事のリライトになります。

## 前提

[【React】Material-UI v4 と Emotion を併用するときの環境構築](https://qiita.com/yuki-endo/items/387194d29a44a3340bb9)の記事のリライトになります。
記事執筆時点で Material UI のバージョンは v5 なります。
npm ではなく yarn を使っています。

## React の環境構築

まずは `create-react-app` の typescript テンプレートを利用してプロジェクトを作成します。今回はプロジェクト名を myapp にします。

```shell
$ npx create-react-app myapp --template typescript
```

## Material UI v5 と Emotion のインストール

スタイリングシステムが変更され `emotion` or `styled-components` が必須になりました。今回はタイトルにある通り `emotion` をインストールします。

```shell
$ yarn add @mui/material @emotion/react @emotion/styled
```

もし `styled-components` を使いたい場合は以下のコマンドでインストールできます。

```shell
$ yarn add @mui/material @mui/styled-engine-sc styled-components
```

一旦動くか確認してみましょう。不要なコードを消して `App.tsx` を以下に変更します。

```tsx:myapp/src/App.tsx
import React from "react";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";

const App: React.FC = () => {
  return (
    <Stack spacing={2} direction="row">
      <Button variant="text">Text</Button>
      <Button variant="contained">Contained</Button>
      <Button variant="outlined">Outlined</Button>
    </Stack>
  );
};

export default App;
```

## スタイリングの方法

v5 から `makeStyle` ではなく `styled` が推奨になったようです。以下の書き方でスタイルが反映されます。

```tsx:myapp/src/App.tsx
import React from "react";
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";

const MyButton = styled(Button)({
  backgroundColor: "red",
  "&:hover": {
    backgroundColor: "red",
  },
});

const App: React.FC = () => {
  return <MyButton variant="contained">ボタン</MyButton>;
};

export default App;
```

また `sx props` というの新しいスタリングの方法が導入されました。ユーティリティファーストな書き方ができます。

```tsx:myapp/src/App.tsx
import React from "react";
import { styled } from "@mui/material/styles";
import Button from "@mui/material/Button";

const App: React.FC = () => {
  return (
    <Button
      variant="contained"
      sx={{
        backgroundColor: "red",
        "&:hover": {
          backgroundColor: "red",
        },
      }}
    >
      ボタン
    </Button>
  );
};

export default App;
```

## Emotion の css prop を使う

公式で推奨されているわけではないですが `css prop` を使いたい方は以下のように書くことができます（個人的に `css prop` を使っているので、以降のスタイリングも `css prop` を使っていきます）。

```tsx:myapp/src/App.tsx
/** @jsxImportSource @emotion/react */
import React from "react";
import { css } from "@emotion/react";
import Button from "@mui/material/Button";

const style = css`
  background-color: red;
  &:hover {
    background-color: red;
  }
`;

const App: React.FC = () => {
  return (
    <Button variant="contained" css={style}>
      ボタン
    </Button>
  );
};

export default App;
```

## CRACO のインストール

`/** @jsxImportSource @emotion/react */` を毎回書くのは面倒なので `CRACO` をインストールします。`CRACO` は create-react-app の設定を上書きするときに使えるライブラリになります。

https://github.com/gsoft-inc/craco

```shell
$ yarn add @craco/craco
```

`craco.config.js` をルートに作り babel の設定を上書きします。

```js:myapp/craco.config.js
module.exports = {
  babel: {
    presets: [
      [
        "@babel/preset-react",
        { runtime: "automatic", importSource: "@emotion/react" },
      ],
    ],
    plugins: ["@emotion/babel-plugin"],
  },
};
```

`App.tsx` ファイルから `/** @jsxImportSource @emotion/react */` の記述を消してもスタイルが反映されるようになりました。

```tsx:myapp/src/App.tsx
import React from "react";
import { css } from "@emotion/react";
import Button from "@mui/material/Button";

const style = css`
  background-color: red;
  &:hover {
    background-color: red;
  }
`;

const App: React.FC = () => {
  return (
    <Button variant="contained" css={style}>
      ボタン
    </Button>
  );
};

export default App;
```

## Theme を Material UI と Emotion で共有する

Emotion の `css prop` で Material UI の Theme を使えるようにします。
まずは `index.tsx` を以下のように書き換えます。

```tsx:myapp/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "@emotion/react";
import {
  createTheme,
  ThemeProvider as MUThemeProvider,
} from "@mui/material/styles";

const theme = createTheme();

ReactDOM.render(
  <React.StrictMode>
    <MUThemeProvider theme={theme}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </MUThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();
```

`emotion.d.ts` ファイルを作成し Emotion の Theme 型に Material UI の Theme 型を継承します。

```ts:myapp/src/types/emotion.d.ts
import { Theme as MUTheme } from "@mui/material/styles";
declare module "@emotion/react" {
  export interface Theme extends MUTheme {}
}
```

`App.tsx` を以下のように書き換えます。Emotion からも Material UI の
Theme が使えるようになっていますね。

```tsx:myapp/src/App.tsx
import React from "react";
import { css, Theme } from "@emotion/react";
import Button from "@mui/material/Button";

const style = (theme: Theme) => css`
  color: ${theme.palette.error.light};
`;

const App: React.FC = () => {
  return (
    <Button variant="contained" css={style}>
      ボタン
    </Button>
  );
};

export default App;
```

## Theme の拡張

次にテーマを拡張したいときの設定方法になります。
テーマの拡張には主に 3 パターンあると思っています。

- Theme の上書きをしたいとき
- Theme に新規項目を加えたいとき
- すでにある Theme を拡張したいとき

順番に書いていきます。

### Theme の上書きをしたいとき

上書きしたいだけならシンプルです。
`createTheme` で該当する値を上書きすれば問題ありません。

```tsx
const theme = createTheme({
  typography: {
    fontFamily: `"Meiryo", "メイリオ", sans-serif`,
  },
});
```

### Theme に新規項目を加えたいとき

次に新しい項目を作成したいときです。
「Theme の上書きをしたいとき」と同様に `createTheme` に新しく加えたい値を書きます。
この時点では型定義のエラーが出るかもしれませんが、次の作業で対応します。

```tsx
const theme = createTheme({
  headerHeight: 100,
});
```

型の拡張をおこなます。
`material-ui.d.ts `という名前（任意の名前で OK）でファイルを作り、以下のように書きます。
`Theme` と `ThemeOptions` の両方で型を拡張します。

```ts:material-ui.d.ts
import { Theme, ThemeOptions } from "'@mui/material/styles";
declare module "@mui/material/styles" {
  interface Theme {
    headerHeight: number;
  }
  interface ThemeOptions {
    headerHeight: number;
  }
}
```

### すでにある Theme を拡張したいとき

基本的には、「Theme に新規項目を加えたいとき」と同じです。
以下は `typography` に `font-size` を計算する新しい関数を追加しています。

```ts
const theme = createTheme({
  typography: {
    size: (n: number) => n * 4,
  },
});
```

```ts:material-ui.d.ts
import {
  Typography,
  TypographyOptions,
} from "@mui/material/styles/createTypography";
declare module "@mui/material/styles/createTypography" {
  interface Typography {
    size: (number) => number;
  }
  interface TypographyOptions {
    size: (number) => number;
  }
}
```

型定義は **myapp/node_modules/@mui/material/styles/createTheme.d.ts** で確認できます。
以下のように型定義されているので、必要そうなところを拡張するといった感じです。

```ts:createTheme.d.ts
/** 略 **/
export interface ThemeOptions extends SystemThemeOptions {
  mixins?: MixinsOptions;
  components?: Components;
  palette?: PaletteOptions;
  shadows?: Shadows;
  transitions?: TransitionsOptions;
  typography?: TypographyOptions | ((palette: Palette) => TypographyOptions);
  zIndex?: ZIndexOptions;
  unstable_strictMode?: boolean;
}

export interface Theme extends SystemTheme {
  mixins: Mixins;
  components?: Components;
  palette: Palette;
  shadows: Shadows;
  transitions: Transitions;
  typography: Typography;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}
/** 略 **/
```

一旦ここまでの対応を踏まえて、以下のように書き換えます。Theme の拡張を行えていることが確認できると思います。

```tsx:myapp/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "@emotion/react";
import {
  createTheme,
  ThemeProvider as MUThemeProvider,
} from "@mui/material/styles";

const theme = createTheme({
  headerHeight: 100,
  typography: {
    fontFamily: `"Meiryo", "メイリオ", sans-serif`,
    size: (n: number) => n * 4,
  },
});

ReactDOM.render(
  <React.StrictMode>
    <MUThemeProvider theme={theme}>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </MUThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();
```

```tsx:myapp/src/App.tsx
import React from "react";
import { css, Theme } from "@emotion/react";
import Button from "@mui/material/Button";

const headerStyle = (theme: Theme) => css`
  height: ${theme.headerHeight}px;
  background: ${theme.palette.primary.main};
`;
const textStyle = (theme: Theme) => css`
  color: ${theme.palette.text.primary};
`;
const buttonStyle = (theme: Theme) => css`
  font-size: ${theme.typography.size(10)}px;
`;

const App: React.FC = () => {
  return (
    <div>
      <header css={headerStyle}>ヘッダー</header>
      <p css={textStyle}>サンプルテキスト</p>
      <Button variant="contained" css={buttonStyle}>
        ボタン
      </Button>
    </div>
  );
};

export default App;
```

```ts:material-ui.d.ts
import { Theme, ThemeOptions } from "'@mui/material/styles";
declare module "@mui/material/styles" {
  interface Theme {
    headerHeight: number;
  }
  interface ThemeOptions {
    headerHeight: number;
  }
}

import {
  Typography,
  TypographyOptions,
} from "@mui/material/styles/createTypography";
declare module "@mui/material/styles/createTypography" {
  interface Typography {
    size: (number) => number;
  }
  interface TypographyOptions {
    size: (number) => number;
  }
}
```

## Global CSS の設定

Global CSS は Emotion の `Global Styles` 機能を使います。

https://emotion.sh/docs/globals

`Global` を Emotion から import して、Global CSS を定義します。

```tsx:myapp/src/styles/GlobalStyles.tsx
import React from "react";
import { Global, css, Theme } from "@emotion/react";

const global = (theme: Theme) => css`
  html,
  body {
    width: 100%;
    height: 100%;
    margin: 0;
    font-family: ${theme.typography.fontFamily};
  }
`;
const GlobalStyles: React.FC = () => {
  return <Global styles={global} />;
};

export default GlobalStyles;
```

上記で作成したファイルを `index.tsx` で読み込むだけで Global な CSS を設定できます。

```tsx:myapp/src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { ThemeProvider } from "@emotion/react";
import {
  createTheme,
  ThemeProvider as MUThemeProvider,
} from "@mui/material/styles";
import GlobalStyles from "./styles/GlobalStyles";

const theme = createTheme({
  headerHeight: 100,
  typography: {
    fontFamily: `"Meiryo", "メイリオ", sans-serif`,
    size: (n: number) => n * 4,
  },
});

ReactDOM.render(
  <React.StrictMode>
    <MUThemeProvider theme={theme}>
      <ThemeProvider theme={theme}>
        <GlobalStyles />
        <App />
      </ThemeProvider>
    </MUThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();
```

## まとめ

Material UI v5 における環境構築の話でした。
v4 から変更された部分も結構ありますので、参考にしていただけると幸いです。

## 参考 URL

https://mui.com/
https://emotion.sh/docs/introduction
https://github.com/gsoft-inc/craco
https://zenn.dev/h_yoshikawa0724/articles/2021-09-26-material-ui-v5