❤️‍🔥

Next.js 13のApp DirectoryとEmotionを組み合わせる

2023/07/27に公開

Next.js 13からの新機能のApp Directoryを使用すると、CSS-in-JSライブラリであるEmotionを動作させることが出来ません。
Next.jsの公式サイトでも現在のところ未サポートとして記載されています。

https://nextjs.org/docs/app/building-your-application/styling/css-in-js

Emotion側でも対応が進められていますが、現時点ではまだリリースされていません。

https://github.com/emotion-js/emotion/issues/2928

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>
  );
};
GitHubで編集を提案

Discussion