You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Remix集成Material UI时出现React Hydration错误求助

Remix + Material UI 服务端渲染 Hydration 错误排查与修复

核心问题定位

你遇到的是服务端与客户端 Emotion 缓存实例未按请求隔离导致的 Hydration 不匹配:首次启动服务器时缓存是全新的,能正常渲染;但后续刷新请求复用了全局缓存实例,导致服务端生成的 CSS 与客户端预期不一致,触发 HTML 不匹配错误。


分步修复方案

1. 为每个请求创建独立的 Emotion 缓存实例

修改 Entry.Server.tsx,确保缓存实例在请求处理函数内部创建,避免模块级全局复用:

// Entry.Server.tsx
import { createCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { RemixServer } from '@remix-run/react';
import { renderToString } from 'react-dom/server';
import type { EntryContext } from '@remix-run/node';

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  // 关键:每个请求单独创建缓存
  const cache = createCache({ key: 'mui-ssr' });
  cache.compat = true;

  const html = renderToString(
    <CacheProvider value={cache}>
      <RemixServer context={remixContext} url={request.url} />
    </CacheProvider>
  );

  // 提取服务端渲染的 CSS 并设置响应头
  const styles = cache.inserted;
  if (styles) {
    responseHeaders.set('X-Emotion-Cache-Key', cache.key);
  }

  return new Response(`<!DOCTYPE html>${html}`, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}

2. 客户端缓存与服务端缓存同步

修改 Entry.Client.tsx,读取服务端返回的缓存标识,确保客户端使用一致的缓存实例:

// Entry.Client.tsx
import { createCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { RemixBrowser } from '@remix-run/react';
import { startTransition, StrictMode } from 'react';
import { hydrateRoot } from 'react-dom/client';

function hydrate() {
  startTransition(() => {
    // 读取服务端传递的缓存 key
    const cacheKey = document.querySelector('meta[name="emotion-cache-key"]')?.getAttribute('content') || 'mui-ssr';
    const cache = createCache({ key: cacheKey });
    cache.compat = true;

    hydrateRoot(
      document,
      <StrictMode>
        <CacheProvider value={cache}>
          <RemixBrowser />
        </CacheProvider>
      </StrictMode>
    );
  });
}

if (window.requestIdleCallback) {
  window.requestIdleCallback(hydrate);
} else {
  setTimeout(hydrate, 1);
}

3. 修正 Root.tsx 中的缓存与主题层级

确保 Material UI 的 ThemeProvider 被正确包裹,且不在 Root 中重复定义 CacheProvider:

// Root.tsx
import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import type { LinksFunction } from '@remix-run/node';
import { Meta, Links, Outlet, ScrollRestoration, Scripts, LiveReload } from '@remix-run/react';

export const links: LinksFunction = () => [
  { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
  { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossOrigin: 'anonymous' },
];

export default function App() {
  const theme = createTheme();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        {/* 新增 meta 标签传递缓存 key 给客户端 */}
        <meta name="emotion-cache-key" content="mui-ssr" />
        <Meta />
        <Links />
      </head>
      <body>
        <ThemeProvider theme={theme}>
          <CssBaseline />
          <Outlet />
        </ThemeProvider>
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

4. 验证修复效果

执行以下步骤确保环境干净:

  • 删除 node_modules:rm -rf node_modules
  • 清空 npm 缓存:npm cache clean --force
  • 重新安装依赖:npm install
  • 重启开发服务器:npm run dev

刷新页面观察错误是否消失,如果仍出现问题,排查以下点:

  • 是否有组件在服务端渲染时调用了 window/document 等客户端专属 API
  • 是否有动态样式的生成逻辑在服务端与客户端不一致
  • 是否有第三方库干扰了 Emotion 缓存的正常工作

内容的提问来源于stack exchange,提问作者Darrell Washington

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 08:40:37