❤️🔥
Next.js 13のApp DirectoryとEmotionを組み合わせる
Next.js 13からの新機能のApp Directoryを使用すると、CSS-in-JSライブラリであるEmotionを動作させることが出来ません。
Next.jsの公式サイトでも現在のところ未サポートとして記載されています。
Emotion側でも対応が進められていますが、現時点ではまだリリースされていません。
Workaround
上記のEmotionリポジトリのIssueに記載されている通り、現時点では以下のようなWorkaroundを使用することでEmotionを使用することが出来ます。
Emotion Cache Providerの作成
src/components/emotion-cache-provider.tsx
'use client';
import { CacheProvider } from '@emotion/react';
import { ReactNode, useState } from 'react';
import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
type EmotionCacheProviderProps = {
children: ReactNode;
};
export const EmotionCacheProvider = ({ children }: EmotionCacheProviderProps) => {
const [registry] = useState(() => {
const cache = createCache({ key: 'css' });
cache.compat = true;
const prevInsert = cache.insert;
let inserted: { name: string; isGlobal: boolean }[] = [];
cache.insert = (...args) => {
const [selector, serialized] = args;
if (cache.inserted[serialized.name] === undefined) {
inserted.push({
name: serialized.name,
isGlobal: !selector,
});
}
return prevInsert(...args);
};
const flush = () => {
const prevInserted = inserted;
inserted = [];
return prevInserted;
};
return { cache, flush };
});
useServerInsertedHTML(() => {
const inserted = registry.flush();
if (inserted.length === 0) {
return null;
}
let styles = '';
let dataEmotionAttribute = registry.cache.key;
const globals: {
name: string;
style: string;
}[] = [];
inserted.forEach(({ name, isGlobal }) => {
const style = registry.cache.inserted[name];
if (typeof style !== 'boolean') {
if (isGlobal) {
globals.push({ name, style });
} else {
styles += style;
dataEmotionAttribute += ` ${name}`;
}
}
});
return (
<>
{globals.map(({ name, style }) => (
<style
key={name}
data-emotion={`${registry.cache.key}-global ${name}`}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: style }}
/>
))}
{styles && (
<style
data-emotion={dataEmotionAttribute}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: styles }}
/>
)}
</>
);
});
return <CacheProvider value={registry.cache}>{children}</CacheProvider>;
};
レイアウトへのEmotion Cache Providerの適用
src/app/layout.tsx
import type { Metadata } from 'next';
import { Noto_Sans_JP } from 'next/font/google';
import { EmotionCacheProvider } from '@/components/emotion-cache-provider';
const inter = Noto_Sans_JP({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<EmotionCacheProvider>
{children}
</EmotionCacheProvider>
</body>
</html>
);
}
ページ内でのEmotionの使用
ページコンポーネントには 'use client';
を追加して明示的にClient Componentであることを宣言する必要があります。
またページ内でEmotionを使用する際には、/** @jsxImportSource @emotion/react */
を追加してEmotionのJSX Pragmaを指定する必要があります。
通常であればnext.config.js
のSWC CompilerのEmotion設定を有効化することで、 /** @jsxImportSource @emotion/react */
を省略することが出来ますが、現時点では動作していないようです。
src/app/page.tsx
'use client';
/** @jsxImportSource @emotion/react */
export default function Home() {
return (
<main>
<p css={{
fontSize: '1.5rem',
}}>
Emotion is working!
</p>
</main>
);
};
Discussion