Next.js 13 App RouterとMUI+Emotion(というよりCSS in JS)
2023/05/09 追記
Next.jsのApp Routerが安定版となったことで当スクラップにたどり着く人が増えているようなので追記。
現時点では当スクラップで書いている通り、クライアントサイドで処理を行うよう明示的に指定する回避策が推奨されているようですが、今後React Server Components(RSC)向けの更新がかかる可能性もあるので今後の動向に期待しましょう。
とはいえEmotion及び依存ライブラリはクライアントサイド動作前提の作りなので時間は掛かりそう...
これから新規開発する際はゼロランタイムにするかどうか選別した方が良さそうです。
2023/07/26 更に追記
MUIに限った話をすると、公式からサンプルが出ているのでそちらを参考にするとよいかと思います。
背景
Next.js + TypeScript + MUI + Emotionでブログを作ろう
↓
折角なら13で追加されたapp directoryを使ってみよう
↓
動かん
そもそもサーバーコンポーネントで動かないことが原因ぽい
css in js 自体がサーバーコンポーネントで使えないって話なのね
解決策としてクライアントコンポーネントにすることが提案されてるけど、その場合appディレクトリの恩恵ってどの程度あるのかな
こちらを参考に実装
原理としてはcssの出力処理をブラウザで行うように明示的に指定して、styleタグとして差し込んであげてるだけ。
'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;
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>
);
}
pageにもuse client
書かないとだめ。
Buttonをラップしてuse client
書いておく形にすればpageに書く必要はない?
'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>
);
}
こんな感じのコンポーネントを作って
'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
の記述外してみたけど動いた。
- '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>
);
}
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を活用できてない)
今更ながら追記。
スタイルがあたるのがクライアント再度でも今までと変わらないし、パフォーマンス面で気になることはなかった。
そして現在では以下のようにドキュメント返却時にもスタイルがあたるようになっている
<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>
というかちゃんと書いてあったねというオチ