📌

Next.jsにMaterial UIを組み込んだ環境を作る

2022/02/11に公開

概要

Next.jsへのMaterial UIの組み込みを行い、簡単に使ってみます。

前提

Next.jsの動作する環境は構築できているものとします。
また、Next.jsはTypeScriptが導入されていることを前提に記載します。

これから環境を作られる方(Windowsの方)は以下参考にしてください。
https://zenn.dev/ttani/articles/wsl2-docker-next-setup

手順

Material UIとスタイリングエンジンのインストール

プロジェクトフォルダ上でnpmかyarnコマンドを実行することで導入できます。
MaterialUIでは、スタイリングエンジンとして、デフォルトがEmotionとなっていますが、styled-componentsも選択できるようになっています。

Linux Emotionを導入する場合
yarn add @mui/material @emotion/react @emotion/styled
Linux styled-componentsを導入する場合
yarn add @mui/material @mui/styled-engine-sc styled-components

ここでは、デフォルトのEmotionで導入します。

Linux Emotionを導入する場合(Dockerコンテナの場合)
docker-compose run -w /usr/app --rm app yarn add @mui/material @emotion/react @emotion/styled

SVGマテリアルアイコンのインストール

SVGマテリアルアイコンもインストールする場合は以下のようになります。

Linux
yarn add @mui/icons-material
Dockerコンテナの場合
docker-compose run -w /usr/app --rm app yarn add @mui/icons-material

emotion/serverのインストール

後述のSSR構成での表示制御で使用するためemotion/serverもインストールします。

Linux
yarn add @emotion/server
Dockerコンテナの場合
docker-compose run -w /usr/app --rm app yarn add @emotion/server

補足:まとめてインストールするコマンド

上記1つずつインストールしましたが、まとめてインストールするコマンドも書いておきます。

Linux
yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material @emotion/server
Dockerコンテナの場合
docker-compose run -w /usr/app --rm app yarn add @mui/material @emotion/react @emotion/styled @mui/icons-material @emotion/server

Next.jsでMaterial UIを使用するための処理を入れる

Next.jsに単純にMaterial UIを導入するだけだと、SSRする関係で、表示制御のタイミングのずれが生じます。
そのため、いくつかのファイルを追加・変更する必要があります。

  • createEmotionCache.ts:キャッシュ関連の共通処理を記載する
  • theme.ts:サイト全体に適用するテーマを設定する
  • pages/_app.tsx:クライアントサイドで必ず読みこまれるファイルを編集する
  • pages/_document.tsx:サーバサイドで必ず読みこまれるファイルを追加して設定する

ほぼ、公式サンプル通りに設定する形で問題ないかと思います。
ただし、いくつか、最新のNex.t.jsだと不要な記述や汎用的ではない記述があるので、そのあたりを修正しています。
https://github.com/mui/material-ui/tree/master/examples/nextjs-with-typescript

createEmotionCache.tsファイルの追加

srcフォルダを追加し、以下の内容のファイルを追加します。このファイルは、_app.tsx_document.tsxから呼ばれることになります。

createEmotionCache.ts
import createCache from '@emotion/cache';

export default function createEmotionCache() {
    return createCache({ key: 'css', prepend: true });
}

theme.tsファイルの追加

以下の内容のファイルを追加します。ここではまだ具体的なtheme内容は設定していませんが、サイト全体のthemeを編集する場合はこのファイルを編集します。

theme.ts
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
    // TODO:テーマ設定を行います
});

export default theme;

pages/_app.tsxの編集

以下のように編集します。デフォルトで呼ばれているstyles/*.css系は使わなくなります。
また、サイト全体にキャッシュ制御とtheme設定が適切に反映されるようにしています。

pages/_app.tsx
import Head from 'next/head';
import type { AppProps } from 'next/app'
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import theme from '../theme';
import createEmotionCache from '../createEmotionCache';

const clientSideEmotionCache = createEmotionCache();
interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  )
}

export default MyApp

pages/_document.tsxの編集

以下のような内容でファイルを追加します。サイト全体のHTML出力関する設定(lang属性やthemeの設定)および、表示タイミングの制御を行う際のサーバサイド側の処理getInitialPropsの定義を行っています。
また、MaterialUIはRobotoというWebFontをベースに作られているため、WebFontのCSSも合わせて読み込みます。MaterialIconについても合わせて読み込んでいます。(MaterialIconを使用する場合)

pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import theme from '../theme';
import createEmotionCache from '../createEmotionCache';

export default class MyDocument extends Document {
    render() {
        return (
            <Html lang="ja">
                <Head>
                    <meta name="theme-color" content={theme.palette.primary.main} />
                    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
                    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
                    {(this.props as any).emotionStyleTags}
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

MyDocument.getInitialProps = async (ctx) => {
    const originalRenderPage = ctx.renderPage;
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App: any) =>
                function EnhanceApp(props) {
                    return <App emotionCache={cache} {...props} />;
                },
        });

    const initialProps = await Document.getInitialProps(ctx)
    const emotionStyles = extractCriticalToChunks(initialProps.html);
    const emotionStyleTags = emotionStyles.styles.map((style) => (
        <style
            data-emotion={`${style.key} ${style.ids.join(' ')}`}
            key={style.key}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: style.css }}
        />
    ));

    return {
        ...initialProps,
        emotionStyleTags,
    };
};

実際に使ってみる

pages/index.tsxファイルを以下のように書き換えます。

pages/index.tsx
import type { NextPage } from 'next'
import { Button } from '@mui/material';

const Home: NextPage = () => {
  return (
    <>
      <Button variant="contained">Hello World</Button>
    </>
  )
}

export default Home

次のようにスタイルが適用されたボタンが表示されれば成功です。

なお、ここではButtonのみ読み込んでいますが、複数種類のコンポーネントを一度importする場合は以下のようになります。

import { Typography, AppBar, Button } from '@mui/material';

所感

1-2年前の情報でも、パッケージ名や設定内容が変わっていて、速いスピードで変化していっていることがうかがえます。ReactやNext.js側も進化しているので、常にキャッチアップして行く必要がありそうですね。

Discussion