😳

Next.jsのlayout.tsx でMUIを使うとHydrationエラーになる?!

に公開

Next.js の layout.tsx で MUI を使うと Hydration エラーになる?

Next.js の App Router を使い、app/layout.tsx に MUI(Material UI)コンポーネントを組み込んで画面を構築すると、開発中に次のようなエラーが発生することがあります。

Error: Hydration failed because the server rendered HTML didn't match the client

サーバーサイドで生成された HTML と、クライアントサイドの React が再レンダリングする際の内容にズレが生じた場合に表示される、React の警告メッセージです。

これは「サーバーとブラウザで表示がズレましたよ!」というReactの警告です。初めて見るとちょっとビックリしますが、原因はだいたい決まっています。
この記事では、このHydrationエラーの原因とその対策を、初心者の私が自分なりに分かりやすくまとめていきたいと思います。


1. Hydration エラーとは?

Next.jsでは、ページの最初の表示(HTML)はサーバーで作って、あとからReactがブラウザ側で動きを追加します。この後半の処理を「Hydration(ハイドレーション)」と呼びます。でも、サーバーが出したHTMLと、Reactが再び表示しようとする内容が違っていたらエラーになります。それが「Hydrationエラー」です。


2. なぜ MUI(Emotion)で発生する?

MUI は内部で Emotion という CSS-in-JS エンジンを使用しています。Emotion はサーバーサイドでスタイルを生成し、クライアントサイドでも同じスタイルを再生成しますが、次のような問題が原因で順序や挿入位置が異なり、DOM の不一致を引き起こします。

  • スタイル挿入順序のズレ: サーバーとクライアントでキャッシュやレンダリングタイミングが異なると、Emotion の <style> タグが異なる順序・場所に追加される。
  • <style><body> 内に挿入される: CSS-in-JS の設定次第で、スタイルタグが <head> ではなく <body> に入ると、描画パフォーマンスやセレクタの適用順序に影響。

3. 対処法:AppRouterCacheProvider を使う

MUI 公式が提供する AppRouterCacheProvider コンポーネントを app/layout.tsx のルートにラップすることで、SSR 時に生成した Emotion のスタイルキャッシュを <head> に正しく注入できます。

// app/layout.tsx
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>
  );
}
  • 効果: サーバー・クライアントで同一の <style> タグ順序を維持し、Hydration 時の不一致を防止。
  • MUI 公式ドキュメントでも推奨されています。

MUI公式ガイドの記述

MUI公式ガイドでは以下の記載があります。

コンポーネントの使用は必須ではありませんがAppRouterCacheProvider、スタイルが に追加され<head>、 でレンダリングされないようにするために、コンポーネントの使用をお勧めします。コンポーネントの使用が推奨される理由については、 https://github.com/mui/material-ui/issues/26561#issuecomment-855286153<body>をご覧ください。

4. 他によくある Hydration エラーのケース

次のような状況でも、サーバーとクライアントの描画結果が異なり、Hydration エラーが発生しやすくなります。

  1. ランダム値の出力
    <p>{Math.random()}</p>
    
  2. window / localStorage などブラウザ専用 API の直接利用
    <p>{window.innerHeight}</p>
    
  3. 条件分岐による表示差
    if (typeof window !== 'undefined') return <ClientOnly />;
    return <ServerPlaceholder />;
    
  4. ロケール依存のフォーマット
    <p>{new Date().toLocaleDateString()}</p>
    
  5. 動的に変化する要素構造
    • 配列の .map()key の付与が不安定、表示順序がサーバー・クライアントで異なる。

5. 動的処理はクライアント側で行うパターン

動的なデータやブラウザ専用 API を扱うコンポーネントは、クライアントコンポーネントとして分離し、useEffect 内で値をセットする方法が安全です。

'use client';
import { useEffect, useState } from 'react';

export default function CurrentTime() {
  const [time, setTime] = useState<string>('');

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);

  return <p>{time}</p>;
}

参考リンク

まとめ

Hydrationエラーは「サーバーとブラウザで表示が違うとき」に起きるエラーです。
特にMUIを使うときは、Emotionのスタイルを正しくSSRすることがとても重要です。
「サーバーでは表示だけ。動的なことはクライアントで!」を意識すれば、エラーを減らせると思います!

アプリ開発サークル@IPUT

Discussion