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
に変更。
{
/** 略 **/
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
}
/** 略 **/
}
ルートディレクトリに以下の設定ファルを作成します。ここで Babel を上書きしています。
module.exports = {
babel: {
presets: [
[
"@babel/preset-react",
{ runtime: "automatic", importSource: "@emotion/react" },
],
],
plugins: ["@emotion/babel-plugin"],
},
};
TypeScript の設定
Emotion に合わせて 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 の ThemeProvider
に createTheme
で作成したテーマを渡します。
css の読み込みなど不要な箇所は削除しています。
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 でテーマを使うときに型がないのでエラーを吐きます。
また補完が効くようになるのでめちゃくちゃ便利です。
import { Theme as MUTheme } from "@material-ui/core";
declare module "@emotion/react" {
export interface Theme extends MUTheme {}
}
これで Emotion から Material-UI のテーマが使えるようになりました。試しに 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
という名前でファイルを作り(任意の名前で問題ないです)、以下のように書きます。
Theme
と ThemeOptions
の両方で型を拡張します。
Theme
は Emotion からテーマを使うときの型定義、ThemeOptions
は createTheme
の引数の型定義です。
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
を計算する新しい関数を追加したいとします。
const theme = createTheme({
typography: {
size: (n: number) => n * 4,
},
});
Typography
と TypographyOptions
に型を追加します。
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
ファイルも忘れずに作成してください)。
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();
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
を更新します。
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 の設定を変更します。
StylesProvider
に injectFirst
をつけてアプリ全体をラップします。
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 を定義します。
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 を設定できます。
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