Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
4
Help us understand the problem. What are the problem?
Organization

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

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

追記

Material UI の v5 がリリースされたのでタイトルを変更しました(v4の追加)。
v5 のリライト記事書きました → Material UI v5 と Emotion の環境構築

前提

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 の設定を上書きするときに使えるやつっぽいです。

そのままだと 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 テーマ機能からも使えるようにします。
Material-UI の MuiThemeProvider と Emotion の ThemeProvidercreateTheme で作成したテーマを渡します。
css の読み込みなど不要な箇所は削除しています。

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 でテーマを使うときに型がないのでエラーを吐きます。
また補完が効くようになるのでめちゃくちゃ便利です。

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 (
    <div>
      <p css={colorStyle}>サンプルテキスト</p>
    </div>
  );
};

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, 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();
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 { 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 機能を使います。

以下のように 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%;
    height: 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 { 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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?