本文へスキップ
ウェブエンジニア問題集
第2章

MUIをNext.js App Routerで使う — セットアップとuse clientの境界

約10分
この章の目次開く

MUIをNext.jsのApp Routerで使うとき、npm install だけでは終わりません。スタイルをサーバー側で正しく組み立てる設定と、Server Componentsとの境界の引き方という2つの作業が必要になります。

この章では、動く状態まで持っていく手順と、それぞれの設定が何を防いでいるのかを押さえます。

学習者学習者

インストールして <Button> を置いたら一応表示されたんだけど、リロードした瞬間だけ見た目が崩れる気がする…。気のせい?

気のせいではありません。まさにこの章で潰す問題です。


インストールするパッケージ

MUIは内部でEmotionというCSS-in-JSライブラリを使っています。そのためMUI本体だけでなく、Emotionも一緒に入れます。

npm install @mui/material @emotion/react @emotion/styled
bash

Next.jsで使う場合は、統合用のパッケージも追加します。

npm install @mui/material-nextjs
bash
パッケージ役割
@mui/materialコンポーネント本体
@emotion/react / @emotion/styledスタイルを生成するエンジン。MUIが依存している
@mui/material-nextjsNext.jsとの統合。App Router用のプロバイダが入っている

AppRouterCacheProviderを置く

app/layout.tsx で、アプリ全体を AppRouterCacheProvider で包みます。

import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <AppRouterCacheProvider>{children}</AppRouterCacheProvider>
      </body>
    </html>
  );
}
tsx

なぜ必要なのか

Next.jsはHTMLを一度に返さず、少しずつ分割して送ります(ストリーミング)。MUIのスタイルはJavaScriptで実行時に作られるため、何も対策しないと、送られてくる断片ごとにスタイルが生成されて <body> の中に散らばります。

AppRouterCacheProvider は、サーバー側で生成されたCSSを集めて <head> にまとめる役割を持ちます。パッケージのソース内にも、これが無い場合は「SSRのたびにコンポーネントごとに新しい <style> タグが生成されてしまう」という説明が書かれています。冒頭の「リロード直後だけ崩れる」はこれが原因です。


'use client' はどこに書くのか

App Routerで一番迷うのがここです。

学習者学習者

MUIを使うファイルには全部 'use client' を書けばいいんでしょ?ネットの記事にそう書いてあったような…。

その理解だと、Server Componentsの利点をほとんど捨てることになります。正確には違います。

MUIのコンポーネントは、ライブラリ側のファイル自身が 'use client' を持っているためです。実際に node_modules/@mui/material/Button/Button.js の先頭を見ると、'use client' が書かれています。つまり Server Componentのファイルから <Button> をimportして置くだけなら、自分のファイルに 'use client' は要りません。

書く必要があるのは、あくまで自分のコードがクライアント側の機能を使うときです。

自分のファイルに 'use client' がケース
要らない<Button variant="contained">送信</Button> のように、静的なpropsだけ渡して置く
要らない<Typography> や <Box> でレイアウトを組む
要るonClick などのイベントハンドラを渡す
要るuseState でダイアログの開閉を管理する
要るuseMediaQuery などMUIのフックを呼ぶ

判断の軸はシンプルで、関数や状態を自分で書くかどうかです。関数はサーバーからクライアントへ渡せないため、イベントハンドラを書いた時点でそのファイルはクライアント側のものになります。

先生先生

「MUIを使うから 'use client'」ではなく「イベントや状態を書くから 'use client'」です。ボタンを置くだけのページはServer Componentのままにしておけます。

境界を設計するイメージ

テーマを置く

色やフォントを変えるには createTheme でテーマを作り、ThemeProvider で配ります。ここにもApp Router特有の注意があります。

createTheme 自体はただの関数で、どこで呼んでも動きます。問題は戻り値です。テーマオブジェクトは内部に関数(theme.spacing() など)を持っており、関数を含むオブジェクトはServer ComponentからClient Componentへpropsとして渡せません。

そのため、テーマの定義とProviderはクライアント側のファイルにまとめます。

// src/theme.ts
'use client';
 
import { createTheme } from '@mui/material/styles';
 
export const theme = createTheme({
  palette: {
    primary: { main: '#1976d2' },
  },
});
tsx
// app/layout.tsx
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { theme } from '@/theme';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <AppRouterCacheProvider>
          <ThemeProvider theme={theme}>
            <CssBaseline />
            {children}
          </ThemeProvider>
        </AppRouterCacheProvider>
      </body>
    </html>
  );
}
tsx

CssBaseline は、ブラウザ標準のスタイルを揃えるためのコンポーネントです。margin: 0 の適用やフォントの統一など、CSSリセットに相当する役割を持ちます。置かないとテーマのフォント設定が効いていないように見えることがあります。


next/fontと組み合わせる

Next.jsのフォント最適化を使う場合は、フォントをCSS変数として読み込み、テーマ側からその変数を参照します。

// app/layout.tsx
import { Roboto } from 'next/font/google';
 
const roboto = Roboto({
  weight: ['400', '500', '700'],
  subsets: ['latin'],
  variable: '--font-roboto',
});
 
// <html> か <body> に roboto.variable を付ける
tsx
// src/theme.ts
export const theme = createTheme({
  typography: {
    fontFamily: 'var(--font-roboto)',
  },
});
tsx

テーマに直接フォント名を書くのではなく、var(--font-roboto) を経由するのがポイントです。Next.jsが実際に読み込んだフォントのファイル名は自動生成されるため、変数で受け取ります。


Tailwind CSSと併用するならenableCssLayer

MUIとTailwindを同じプロジェクトで使うと、どちらのスタイルが勝つかで揉めます。MUIのスタイルは詳細度が高く、Tailwindのクラスを当てても効かないことがあります。

このときは enableCssLayer を有効にします。

<AppRouterCacheProvider options={{ enableCssLayer: true }}>{children}</AppRouterCacheProvider>
tsx

これでMUIのスタイルが @layer mui というカスケードレイヤーに入ります。レイヤーに入ったスタイルは、レイヤー外のスタイルより優先度が低くなるため、Tailwindのクラスで上書きできるようになります。

このサイト自身がこの設定です。デザインはTailwindで作り、管理画面のテーブルやダイアログにだけMUIを使っています。詳しい仕組みは、他のCSSと共存するで扱います。


よくあるハマりどころ

初回表示の一瞬だけスタイルが崩れる

AppRouterCacheProvider を置いていないケースがほとんどです。開発中は気づきにくく、本番で目立ちます。

「Functions cannot be passed directly to Client Components」というエラー

Server Componentのファイルで onClick={...} を書いています。そのコンポーネントを別ファイルに切り出して 'use client' を付けるか、ファイルごとクライアント側にします。

テーマを変えたのに反映されない

ThemeProvider で包む範囲の外にコンポーネントがある可能性があります。layout.tsx のできるだけ外側で包んでください。

学習者学習者

エラーメッセージが「Functions cannot be passed〜」だったら、MUIじゃなくてServer Componentsの話だと考えればいいのね。

そのとおりです。MUI特有の問題とApp Routerの問題を切り分けられると、原因にたどり着くのが早くなります。


ちゃんと使うためのポイント

  • MUIはEmotionに依存しているので、@emotion/react と @emotion/styled も一緒に入れる
  • AppRouterCacheProvider はSSRのCSSを <head> にまとめる。無いと初回表示が崩れる
  • MUIのコンポーネントは自前で 'use client' を持っている。使うだけなら自分のファイルに書く必要はない
  • 自分のファイルに 'use client' が要るのは、イベントハンドラ・state・フックを書くとき
  • テーマは関数を含むので、定義ファイル側に 'use client' を付ける
  • Tailwindと併用するなら enableCssLayer: true

次の章では、MUIのスタイリングの基本単位であるsx propの使い方を扱います。v9で書き方が変わった部分でもあります。


参考リンク