share facebook facebook facebook twitter twitter menu hatena pocket slack

2021.08.30 MON

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

遠藤 優輝

WRITTEN BY 遠藤 優輝

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

前提

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

React の環境構築

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

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

Emotion のインストール

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

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

CRACO のインスール

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

Create React App Configuration Override, an easy and comprehensible configuration layer for create-react-app - GitHub - gsoft-inc/craco: Create React App Configuration Override, an easy and compreh...

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

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

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

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

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

$ yarn add @craco/craco

npm-scripts を CRACO に変更。

myapp/package.json

{
  /** 略 **/
  "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test"
  }
  /** 略 **/
}

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

myapp/craco.config.js

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

TypeScript の設定

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

myapp/tsconfig.json

{
  "compilerOptions": {
    /** 略 **/
    "jsxImportSource": "@emotion/react"
  }
  /** 略 **/
}

Material-UI のインストール

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

$ yarn add @material-ui/core

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

Theme の設定

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

myapp/src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createTheme } from "@material-ui/core/styles";
import { ThemeProvider } from "@emotion/react";
import reportWebVitals from "./reportWebVitals";

const theme = createTheme();

ReactDOM.render(
  ,
  document.getElementById("root")
);

reportWebVitals();

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

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 を以下のように修正しましょう。

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 (
    

サンプルテキスト

); }; export default App;

Theme の拡張

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

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

順番に書いていきます。

Theme の上書きをしたいとき

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

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

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

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

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

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

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 に新規項目を加えたいとき」と同じです。
例えば typographyfont-size を計算する新しい関数を追加したいとします。

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

TypographyTypographyOptions に型を追加します。

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 で確認できます。
以下のように型定義されているので、あとは必要そうなところを拡張するといった感じです。

/** 略 **/
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 ファイルも忘れずに作成してください)。

myapp/src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createTheme } 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>
    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();

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 を更新します。

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 タグよりも後ろに記述されるためです。
この問題を解決するためには別途設定が必要です。

CSS injection orderの設定を変更します。
StylesProviderinjectFirst をつけてアプリ全体をラップします。

myapp/src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { ThemeProvider } from "@emotion/react";
import { StylesProvider } from "@material-ui/core/styles";
import { createTheme } 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>
      <ThemeProvider theme={theme}>
        <App />
      </ThemeProvider>
    </StylesProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();

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

Global CSS の設定

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

以下のように Global を Emotion から import して、Global CSS を定義します。

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%;
    hegiht: 100%;
    margin: 0;
    font-family: ${theme.typography.fontFamily};
  }
`;
const GlobalStyles: React.FC = () => {
  return <Global styles={global} />;
};

export default GlobalStyles;

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

myapp/src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { StylesProvider } from "@material-ui/core/styles";
import { createTheme } 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>
      <ThemeProvider theme={theme}>
        <GlobalStyles />
        <App />
      </ThemeProvider>
    </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

元記事はこちら

https://qiita.com/yuki-endo/items/387194d29a44a3340bb9

遠藤 優輝

遠藤 優輝

streampackチーム所属。グミが好きです。

cloudpack

cloudpackは、Amazon EC2やAmazon S3をはじめとするAWSの各種プロダクトを利用する際の、導入・設計から運用保守を含んだフルマネージドのサービスを提供し、バックアップや24時間365日の監視/障害対応、技術的な問い合わせに対するサポートなどを行っております。
AWS上のインフラ構築およびAWSを活用したシステム開発など、案件のご相談はcloudpack.jpよりご連絡ください。