Closed8

Next.js 13 App RouterとMUI+Emotion(というよりCSS in JS)

takecchitakecchi

2023/05/09 追記

Next.jsのApp Routerが安定版となったことで当スクラップにたどり着く人が増えているようなので追記。
現時点では当スクラップで書いている通り、クライアントサイドで処理を行うよう明示的に指定する回避策が推奨されているようですが、今後React Server Components(RSC)向けの更新がかかる可能性もあるので今後の動向に期待しましょう。

とはいえEmotion及び依存ライブラリはクライアントサイド動作前提の作りなので時間は掛かりそう...
これから新規開発する際はゼロランタイムにするかどうか選別した方が良さそうです。
https://github.com/emotion-js/emotion/issues/2928

2023/07/26 更に追記

MUIに限った話をすると、公式からサンプルが出ているのでそちらを参考にするとよいかと思います。

背景

Next.js + TypeScript + MUI + Emotionでブログを作ろう

折角なら13で追加されたapp directoryを使ってみよう

動かん

takecchitakecchi

こちらを参考に実装
https://github.com/emotion-js/emotion/issues/2928#issuecomment-1293012737

原理としてはcssの出力処理をブラウザで行うように明示的に指定して、styleタグとして差し込んであげてるだけ。

src/app/registry.tsx
'use client';

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { createTheme, ThemeProvider } from '@mui/material';

const theme = createTheme();

const EmotionRegistry = ({ children }: { children: React.ReactNode }) => {
  const [emotionCache] = useState(() => {
    const emotionCache = createCache({ key: 'css', prepend: true });
    emotionCache.compat = true;
    return emotionCache;
  });

  useServerInsertedHTML(() => {
    return (
      <style
        data-emotion={`${emotionCache.key} ${Object.keys(
          emotionCache.inserted
        ).join(' ')}`}
        dangerouslySetInnerHTML={{
          __html: Object.values(emotionCache.inserted).join(' '),
        }}
      />
    );
  });
  if (typeof window !== 'undefined') return <>{children}</>;

  return (
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={theme}>{children}</ThemeProvider>
    </CacheProvider>
  );
};

export default EmotionRegistry;
src/app/layout.tsx
import '@/styles/globals.css';
import EmotionRegistry from '@/app/registry';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <head />
      <body>
        <EmotionRegistry>{children}</EmotionRegistry>
      </body>
    </html>
  );
}
takecchitakecchi

pageにもuse client書かないとだめ。
Buttonをラップしてuse client書いておく形にすればpageに書く必要はない?

src/app/page.tsx
'use client';

import Image from 'next/image';
import { Inter } from '@next/font/google';
import styles from '@/styles/Home.module.css';
import { Button } from '@mui/material';

const inter = Inter({ subsets: ['latin'] });

export default function Home() {
  return (
    <main className={styles.main}>
      <Button>EXAMPLE</Button>
    </main>
  );
}
takecchitakecchi

こんな感じのコンポーネントを作って

src/app/MuiButton.tsx
'use client';

import React from 'react';
import { Button } from '@mui/material';

const MuiButton = ({ children }: { children: React.ReactNode }) => (
  <Button>{children}</Button>
);
export default MuiButton;

page.tsxからuse clientの記述外してみたけど動いた。

src/app/page.tsx
- 'use client';

import Image from 'next/image';
import { Inter } from '@next/font/google';
import styles from '@/styles/Home.module.css';
- import { Button } from '@mui/material';
+ import { MuiButton } from '@/app/MuiButton';

const inter = Inter({ subsets: ['latin'] });

export default function Home() {
  return (
    <main className={styles.main}>
-       <Button>EXAMPLE</Button>
+       <MuiButton>EXAMPLE</MuiButton>
    </main>
  );
}
takecchitakecchi

localhost:3000を確認してみるとdocumentとして以下のように返却されていた。(必要な部分のみ抜粋)
ドキュメントが返却される時点でclass指定はされた状態であることがわかります。

    <body>
    <main class="Home_main__EtNt2">
        <button
            class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium css-1e6y48t-MuiButtonBase-root-MuiButton-root"
            tabindex="0" type="button">EXAMPLE
        </button>
    </main>
    <script src="/_next/static/chunks/webpack.js" async=""></script>
    <script src="/_next/static/chunks/main-app.js" async=""></script>
    </body>

ただし、スタイルを当てるのはクライアントサイドになるため相性は悪そう。(RSCを活用できてない)

takecchitakecchi

今更ながら追記。
スタイルがあたるのがクライアント再度でも今までと変わらないし、パフォーマンス面で気になることはなかった。
そして現在では以下のようにドキュメント返却時にもスタイルがあたるようになっている

<style data-emotion="mui-global o6gwfi">
    html {
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        box-sizing: border-box;
        -webkit-text-size-adjust: 100%;
    }

    *,*::before,*::after {
        box-sizing: inherit;
    }

    strong,b {
        font-weight: 700;
    }

    body {
        margin: 0;
        color: rgba(0, 0, 0, 0.87);
        font-family: "Roboto","Helvetica","Arial",sans-serif;
        font-weight: 400;
        font-size: 1rem;
        line-height: 1.5;
        letter-spacing: 0.00938em;
        background-color: #fff;
    }

    @media print {
        body {
            background-color: #fff;
        }
    }

    body::backdrop {
        background-color: #fff;
    }
</style>
このスクラップは2023/01/20にクローズされました