【React】Material-UI v4 と Emotion を併用するときの環境構築

React (TypeScript) のプロジェクトにおいて Material-UI と Emotion (CSS in JS) を併用するときに、役に立つかもしれない準備的な話になります。

## 追記
Material UI の v5 がリリースされたのでタイトルを変更しました（v4の追加）。 
v5 のリライト記事書きました → [Material UI v5 と Emotion の環境構築](https://qiita.com/yuki-endo/items/f79886407add408c72c1)。

## 前提

create-react-app の TypeScript テンプレートを使います。
npm ではなく yarn を使っています（npm でも同様の手順を踏めば動くと思いますが、確認していません）。

## React の環境構築

プロジェクト名は適宜変更してください。今回は myapp にします。

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

## Emotion のインストール

Emotion をインストールします。

```shell
$ yarn add @emotion/react @emotion/babel-plugin
```

## CRACO のインスール

`CRACO` なにこれって感じですね。簡単にいうと create-react-app の設定を上書きするときに使えるやつっぽいです。

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

そのままだと Emotion の書き方がめんどくさいので、Babel の設定を上書きするために使います。

```tsx
/** @jsx jsx */
import { css, jsx } from "@emotion/react";
```

デフォルトだと ↑ の記述を、 ↓ の記述で OK にするための方法です。

```tsx
import { css } from "@emotion/react";
```

まずは `CRACO` のインストール。

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

npm-scripts を `CRACO` に変更。

```json:myapp/package.json
{
  /** 略 **/
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test"
  }
  /** 略 **/
}
```

ルートディレクトリに以下の設定ファルを作成します。ここで Babel を上書きしています。

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

## TypeScript の設定

Emotion に合わせて `tsconfig.json` を修正します。
といっても 以下の記述を加えるだけです。

```json:myapp/tsconfig.json
{
  "compilerOptions": {
    /** 略 **/
    "jsxImportSource": "@emotion/react"
  }
  /** 略 **/
}
```

## Material-UI のインストール

Material-UI をインストールしましょう。

```shell
$ yarn add @material-ui/core
```

ここまでで、Emotion と Material-UI が使えるようになりました。ただし、併用するにはもう少し準備した方がいいです。

## Theme の設定

Material-UI のテーマを Emotion テーマ機能からも使えるようにします。
Material-UI の `MuiThemeProvider` と Emotion の `ThemeProvider` に `createTheme` で作成したテーマを渡します。
css の読み込みなど不要な箇所は削除しています。

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

const theme = createTheme();

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

reportWebVitals();
```

Emotion の型を Material-UI の型から拡張します。
この作業をしないと、Emotion でテーマを使うときに型がないのでエラーを吐きます。
また補完が効くようになるのでめちゃくちゃ便利です。

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

これで Emotion から Material-UI のテーマが使えるようになりました。試しに App.tsx を以下のように修正しましょう。

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

const textStyle = (theme: Theme) => css`
  color: ${theme.palette.text.primary};
`;

const App: React.FC = () => {
  return (
    <div>
      <p css={colorStyle}>サンプルテキスト</p>
    </div>
  );
};

export default App;
```

### Theme の拡張

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

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

順番に書いていきます。

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

単純に上書きしたいだけのときは簡単です。
`createTheme` で該当する値を上書きすれば問題ありません。以下は `font-famiy` を上書きするときの例です。

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

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

次に新しい項目を作成したいときです。
`createTheme` に新しく加えたい値を書きます。ここまでは上書きと同じです。
例えばヘッダーの高さをテーマで管理したいときは以下のように書きます。この時点で型定義のエラーが出るかもしれませんが、次の作業で対応します。

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

次に型の拡張を行う必要があります。
`material-ui.d.ts` という名前でファイルを作り（任意の名前で問題ないです）、以下のように書きます。
`Theme` と `ThemeOptions` の両方で型を拡張します。
`Theme` は Emotion からテーマを使うときの型定義、`ThemeOptions` は `createTheme` の引数の型定義です。

```ts:myapp/src/types/material-ui.d.ts
import { Theme, ThemeOptions } from "'@material-ui/core/styles/createMuiTheme";
declare module "@material-ui/core/styles/createTheme" {
  interface Theme {
    headerHeight: number;
  }
  interface ThemeOptions {
    headerHeight: number;
  }
}
```

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

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

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

`Typography` と `TypographyOptions` に型を追加します。

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

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

```ts
/** 略 **/
export interface ThemeOptions {
  shape?: ShapeOptions;
  breakpoints?: BreakpointsOptions;
  direction?: Direction;
  mixins?: MixinsOptions;
  overrides?: Overrides;
  palette?: PaletteOptions;
  props?: ComponentsProps;
  shadows?: Shadows;
  spacing?: SpacingOptions;
  transitions?: TransitionsOptions;
  typography?: TypographyOptions | ((palette: Palette) => TypographyOptions);
  zIndex?: ZIndexOptions;
  unstable_strictMode?: boolean;
}

export interface Theme {
  shape: Shape;
  breakpoints: Breakpoints;
  direction: Direction;
  mixins: Mixins;
  overrides?: Overrides;
  palette: Palette;
  props?: ComponentsProps;
  shadows: Shadows;
  spacing: Spacing;
  transitions: Transitions;
  typography: Typography;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}
/** 略 **/
```

型の拡張方法に関しては Material-UI の公式サイトにも説明があるので記載しておきます。
https://v4-9-14.material-ui.com/guides/typescript/#customization-of-theme

## Material-UI の CSS を上書きするための設定

先ほど「Theme の拡張」で挙げたを例を`index.tsx`に反映するとこうなります( `material-ui.d.ts` ファイルも忘れずに作成してください)。

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

const theme = createTheme({
  headerHeight: 100,
  typography: {
    fontFamily: `"Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN", "Hiragino Sans", "ヒラギノ角ゴシック", "Meiryo", "メイリオ", sans-serif`,
    size: (n: number) => n * 4,
  },
});

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

reportWebVitals();
```

```ts:myapp/src/types/material-ui.d.ts
import { Theme, ThemeOptions } from "'@material-ui/core/styles/createMuiTheme";
declare module "@material-ui/core/styles/createTheme" {
  interface Theme {
    headerHeight: number;
  }
  interface ThemeOptions {
    headerHeight: number;
  }
}
import {
  Typography,
  TypographyOptions,
} from "@material-ui/core/styles/createTypography";
declare module "@material-ui/core/styles/createTypography" {
  interface Typography {
    size: (number) => number;
  }
  interface TypographyOptions {
    size: (number) => number;
  }
}

```

ここまでできたら、以下のように `App.tsx` を更新します。

```tsx:myapp/src/App.tsx
import React from "react";
import { css, Theme } from "@emotion/react";
import Button from "@material-ui/core/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" color="primary" css={buttonStyle}>
        ボタン
      </Button>
    </div>
  );
};

export default App;
```

基本的には問題ないように思えますが、一箇所だけ期待通りでない挙動をしている箇所があります。`Button` のスタイルが上書きされていません。
これは Material-UI の style タグが Emotion で書いた style タグよりも後ろに記述されるためです。
この問題を解決するためには別途設定が必要です。

https://material-ui.com/styles/advanced/#css-injection-order

**CSS injection order** の設定を変更します。
`StylesProvider` に `injectFirst` をつけてアプリ全体をラップします。

```tsx:myapp/src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { ThemeProvider } from "@emotion/react";
import { createTheme, MuiThemeProvider, StylesProvider } from "@material-ui/core/styles";
import reportWebVitals from "./reportWebVitals";

const theme = createTheme({
  headerHeight: 100,
  typography: {
    fontFamily: `"Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN", "Hiragino Sans", "ヒラギノ角ゴシック", "Meiryo", "メイリオ", sans-serif`,
    size: (n: number) => n * 4,
  },
});

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

reportWebVitals();
```

以上で再度リロードするとボタンの CSS が上書きされているのが分かります。
ただしこの方法はあくまで CSS の style タグの順番を入れ替えているだけです。
したがって、詳細度の壁は越えられませんので気をつけましょう。

## 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 { createTheme, MuiThemeProvider, StylesProvider } from "@material-ui/core/styles";
import { ThemeProvider } from "@emotion/react";
import GlobalStyles from "./styles/GlobalStyles";
import reportWebVitals from "./reportWebVitals";

const theme = createTheme({
  headerHeight: 100,
  typography: {
    fontFamily: `"Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN", "Hiragino Sans", "ヒラギノ角ゴシック", "Meiryo", "メイリオ", sans-serif`,
    size: (n: number) => n * 4,
  },
});

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

以上で準備が完了しました。あとは好きなようにスタイリングしましょう。

## まとめ

React(TypeScript)プロジェクトにおいて、Material-UI と Emotion を併用したいときの環境構築の話でした。
慣れないうちは色々設定に戸惑うと思うので、是非参考にしていただければと思います。

## 参考 URL

https://material-ui.com/
https://emotion.sh/docs/introduction
https://github.com/gsoft-inc/craco
https://qiita.com/xrxoxcxox/items/17e0762d8e69c1ef208f