Claude
Skills
Sign in
Back

ssr-nextjs

Included with Lifetime
$97 forever

MUI server-side rendering with Next.js — App Router, Pages Router, RSC compatibility, Emotion cache, and Pigment CSS

Web Dev

What this skill does


# MUI SSR with Next.js

## 1. Next.js App Router Setup (v13+)

The App Router requires a client-side ThemeRegistry component that flushes Emotion's
server-generated styles into the document head via `useServerInsertedHTML`.

### ThemeRegistry client component

```tsx
// src/components/ThemeRegistry/EmotionCache.tsx
'use client';

import * as React from 'react';
import createCache from '@emotion/cache';
import { useServerInsertedHTML } from 'next/navigation';
import { CacheProvider } from '@emotion/react';

export default function NextAppDirEmotionCacheProvider(
  props: { options: Parameters<typeof createCache>[0]; children: React.ReactNode }
) {
  const { options, children } = props;

  const [registry] = React.useState(() => {
    const cache = createCache(options);
    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);
    };

    return { cache, flush: () => { const prev = inserted; inserted = []; return prev; } };
  });

  useServerInsertedHTML(() => {
    const names = registry.flush();
    if (names.length === 0) return null;

    let styles = '';
    let dataEmotionAttribute = registry.cache.key;
    const globals: { name: string; style: string }[] = [];

    for (const { name, isGlobal } of names) {
      const style = registry.cache.inserted[name];
      if (typeof style === 'string') {
        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`}
            dangerouslySetInnerHTML={{ __html: style }}
          />
        ))}
        {styles && (
          <style
            data-emotion={dataEmotionAttribute}
            dangerouslySetInnerHTML={{ __html: styles }}
          />
        )}
      </>
    );
  });

  return <CacheProvider value={registry.cache}>{children}</CacheProvider>;
}
```

### ThemeRegistry wrapper

```tsx
// src/components/ThemeRegistry/ThemeRegistry.tsx
'use client';

import * as React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import NextAppDirEmotionCacheProvider from './EmotionCache';
import theme from './theme';

export default function ThemeRegistry({ children }: { children: React.ReactNode }) {
  return (
    <NextAppDirEmotionCacheProvider options={{ key: 'mui', prepend: true }}>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        {children}
      </ThemeProvider>
    </NextAppDirEmotionCacheProvider>
  );
}
```

### app/layout.tsx integration

```tsx
// app/layout.tsx
import ThemeRegistry from '@/components/ThemeRegistry/ThemeRegistry';

export const metadata = {
  title: 'My App',
  description: 'MUI + Next.js App Router',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <ThemeRegistry>{children}</ThemeRegistry>
      </body>
    </html>
  );
}
```

### Required dependencies

```bash
npm install @mui/material @emotion/react @emotion/styled @emotion/cache
```

---

## 2. Next.js Pages Router Setup

The Pages Router uses `_document.tsx` to extract critical CSS at render time and
inject it into the initial HTML response.

### createEmotionCache utility

```tsx
// src/createEmotionCache.ts
import createCache from '@emotion/cache';

export default function createEmotionCache() {
  return createCache({ key: 'css', prepend: true });
}
```

### _app.tsx with CacheProvider

```tsx
// pages/_app.tsx
import * as React from 'react';
import Head from 'next/head';
import { AppProps } from 'next/app';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { CacheProvider, EmotionCache } from '@emotion/react';
import theme from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

interface MyAppProps extends AppProps {
  emotionCache?: EmotionCache;
}

export default function MyApp(props: MyAppProps) {
  const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
  return (
    <CacheProvider value={emotionCache}>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}
```

### _document.tsx with extractCriticalToChunks

```tsx
// pages/_document.tsx
import * as React from 'react';
import Document, {
  Html,
  Head,
  Main,
  NextScript,
  DocumentProps,
  DocumentContext,
} from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import { AppType } from 'next/app';
import theme from '../src/theme';
import createEmotionCache from '../src/createEmotionCache';

interface MyDocumentProps extends DocumentProps {
  emotionStyleTags: React.ReactElement[];
}

export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
  return (
    <Html lang="en">
      <Head>
        <meta name="theme-color" content={theme.palette.primary.main} />
        <link rel="shortcut icon" href="/favicon.ico" />
        {emotionStyleTags}
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

MyDocument.getInitialProps = async (ctx: DocumentContext) => {
  const originalRenderPage = ctx.renderPage;
  const cache = createEmotionCache();
  const { extractCriticalToChunks } = createEmotionServer(cache);

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App: React.ComponentType<React.ComponentProps<AppType> & { emotionCache: ReturnType<typeof createEmotionCache> }>) =>
        function EnhanceApp(props) {
          return <App emotionCache={cache} {...props} />;
        },
    });

  const initialProps = await Document.getInitialProps(ctx);
  const emotionStyles = extractCriticalToChunks(initialProps.html);
  const emotionStyleTags = emotionStyles.styles.map((style) => (
    <style
      data-emotion={`${style.key} ${style.ids.join(' ')}`}
      key={style.key}
      dangerouslySetInnerHTML={{ __html: style.css }}
    />
  ));

  return {
    ...initialProps,
    emotionStyleTags,
  };
};
```

### Additional dependency for Pages Router

```bash
npm install @emotion/server
```

---

## 3. Server Components Compatibility

### MUI components do NOT work in React Server Components

Every MUI component uses React context (ThemeProvider), hooks (useTheme, useState),
or event handlers. **None of them can be rendered as RSC.** Any file importing from
`@mui/material` must include `'use client'` at the top, or be imported from a file
that does.

### Client boundary patterns

**Pattern 1: Thin client wrapper around server data**

```tsx
// app/users/page.tsx (Server Component — fetches data)
import UserTable from './UserTable';

export default async function UsersPage() {
  const users = await db.user.findMany(); // server-side data fetch
  return <UserTable users={users} />;     // pass plain data to client
}
```

```tsx
// app/users/UserTable.tsx (Client Component — renders MUI)
'use client';

import {
  Table, TableBody, TableCell, TableContainer,
  TableHead, TableRow, Paper
} from '@mui/material';

interface User { id: string; name: string; email: string; }

export default function UserTable({ users }: { users: User[] }) {
  

Related in Web Dev