Настройка SSR с MUI

Для использования Material-UI (MUI) в проектах с серверным рендерингом (SSR, Server-Side Rendering) необходимо обеспечить корректное управление стилями на сервере и клиенте. MUI использует механизм генерации CSS через Emotion (или JSS в старых версиях), что требует особого подхода при SSR, чтобы избежать несоответствия стилей между серверной и клиентской отрисовкой.

Шаг 1. Установка зависимостей

Для проекта на React с SSR (например, на Next.js или Express) потребуются следующие пакеты:

npm install @mui/material @emotion/react @emotion/server @emotion/styled
  • @mui/material – сама библиотека компонентов.
  • @emotion/react – библиотека для работы с CSS-in-JS.
  • @emotion/server – инструменты для генерации критических CSS на сервере.
  • @emotion/styled – утилиты для стилизованных компонентов.

Шаг 2. Создание Emotion Cache

MUI рекомендует создавать отдельный Emotion cache для сервера и клиента. Это позволяет корректно собирать и инжектить стили.

// createEmotionCache.js
import createCache from '@emotion/cache';

export default function createEmotionCache() {
  return createCache({ key: 'css', prepend: true });
}
  • key: 'css' определяет префикс для всех классов.
  • prepend: true гарантирует, что стили Emotion будут вставлены перед стилями других библиотек, предотвращая конфликт.

Шаг 3. Настройка SSR на сервере

Для сервера важно собрать все стили до отправки HTML клиенту. Пример на Express:

import { renderToString } from 'react-dom/server';
import { CacheProvider } from '@emotion/react';
import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache from './createEmotionCache';
import App from './App';

app.get('*', (req, res) => {
  const cache = createEmotionCache();
  const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

  const html = renderToString(
    <CacheProvider value={cache}>
      <App />
    </CacheProvider>
  );

  const emotionChunks = extractCriticalToChunks(html);
  const emotionCss = constructStyleTagsFromChunks(emotionChunks);

  res.send(`
    <!DOCTYPE html>
    <html lang="ru">
      <head>
        ${emotionCss}
      </head>
      <body>
        <div id="root">${html}</div>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `);
});
  • CacheProvider обеспечивает передачу единого cache для компонентов MUI.
  • extractCriticalToChunks и constructStyleTagsFromChunks генерируют критические CSS для серверного рендеринга.
  • Вставка <style> до загрузки клиентского JavaScript предотвращает FOUC (Flash of Unstyled Content).

Шаг 4. Настройка на клиенте

На клиенте необходимо повторно использовать тот же cache для предотвращения повторного генерации CSS и конфликта классов:

import { CacheProvider } from '@emotion/react';
import createEmotionCache from './createEmotionCache';
import ReactDOM from 'react-dom';
import App from './App';

const cache = createEmotionCache();

ReactDOM.hydrate(
  <CacheProvider value={cache}>
    <App />
  </CacheProvider>,
  document.getElementById('root')
);
  • ReactDOM.hydrate используется вместо render для сохранения состояния, сгенерированного на сервере.

Шаг 5. Интеграция с Next.js

Next.js имеет встроенные возможности SSR, поэтому интеграция с MUI немного отличается:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache from '../src/createEmotionCache';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);
    
    const originalRenderPage = ctx.renderPage;

    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) => <App emotionCache={cache} {...props} />,
      });

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

    return {
      ...initialProps,
      styles: [...initialProps.styles, ...emotionCss],
    };
  }

  render() {
    return (
      <Html lang="ru">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}
  • enhanceApp позволяет передать свой Emotion cache в корневой компонент.
  • Все критические стили инжектируются в <Head> на этапе SSR.

Шаг 6. Избежание проблем с перезаписью стилей

  • Всегда использовать отдельный cache для сервера и клиента.
  • Не смешивать MUI v5 и старую JSS версию, чтобы избежать конфликта CSS.
  • Вставка <style> в <head> до <body> предотвращает FOUC.

Шаг 7. Настройка темы и глобальных стилей

Для SSR важно также передавать тему MUI на сервер:

import { ThemeProvider, createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: { main: '#1976d2' },
    secondary: { main: '#dc004e' },
  },
});

<ThemeProvider theme={theme}>
  <App />
</ThemeProvider>
  • Тема должна быть одинаковой на сервере и клиенте для синхронизации стилей.
  • Глобальные стили можно задавать через GlobalStyles или CssBaseline.

Ключевые моменты

  • Emotion cache – обязательный для SSR, предотвращает расхождение стилей.
  • extractCriticalToChunks – генерирует CSS, который нужен для корректной серверной отрисовки.
  • ReactDOM.hydrate – обеспечивает сохранение состояния и правильное подключение стилей на клиенте.
  • Тема MUI должна быть идентичной на сервере и клиенте для единообразного внешнего вида.
  • Корректная структура <head> и <style> предотвращает FOUC и ошибки при гидрации компонентов.

Настройка SSR с MUI позволяет создавать полностью серверно-рендеренные приложения с корректной загрузкой стилей, быстрой первоначальной отрисовкой и минимизацией визуальных сбоев при гидрации на клиенте.