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

Next.js+Material UI+react-apollo SSR配置问题:className不匹配修复

解决Next.js + Material UI + React Apollo的服务端样式不匹配与查询异常问题

首先,咱们先拆解你遇到的两个核心问题:服务端与客户端的className不匹配(导致图标样式异常),以及Apollo查询异常引发的UI渲染差异。这两个问题往往会互相影响,下面给你一步步的修复方案:

一、统一ServerStyleSheets与getDataFromTree的配置

你现在在_document.js里分别创建了两个ServerStyleSheets实例,一个用于渲染,一个用于getDataFromTree,这很可能是样式类名不匹配的关键原因。正确的做法是在同一次流程中复用同一个样式收集实例:

修改_document.js的getInitialProps方法:

import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '@material-ui/styles';
import React from 'react';
import { getDataFromTree } from '@apollo/react-ssr';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    // 创建唯一的样式收集实例,不要拆分创建
    const sheets = new ServerStyleSheets({ injectFirst: true });
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
        });

      // 先获取基础页面props
      const appProps = await Document.getInitialProps(ctx);
      // 复用同一个sheets实例执行getDataFromTree,确保样式和数据预取同步
      await getDataFromTree(sheets.collect(<App {...appProps.pageProps} />));

      return {
        ...appProps,
        styles: [...React.Children.toArray(appProps.styles), sheets.getStyleElement()],
      };
    } finally {
      // 可选:清理资源
    }
  }

  render() {
    return (
      <Html lang="en">
        <Head>
          {/* 自定义head内容 */}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

关键要点:

  • 只创建一个ServerStyleSheets实例,同时用于页面渲染增强和getDataFromTree的组件包裹
  • 保留injectFirst: true,让Material UI的样式能覆盖Next.js默认样式,避免样式优先级问题
  • 绝对不要在getDataFromTree时使用disableGeneration: true,这个选项会关闭服务端样式生成,直接导致客户端和服务端样式类名完全不匹配

二、修复Apollo查询异常导致的渲染差异

当Apollo查询返回undefined数据时,直接展开会导致服务端和客户端渲染的DOM结构不一致,触发React的hydration警告,同时放大样式不匹配的问题(比如图标没有正确的类名)。你需要做防御性渲染:

  1. 在组件中明确检查数据存在性再渲染:
import { useQuery } from '@apollo/react-hooks';
import MY_QUERY from './queries/myQuery';
import { SvgIcon } from '@material-ui/core';

function MyComponent() {
  const { loading, error, data } = useQuery(MY_QUERY);

  if (loading) return <div>加载中...</div>;
  // 明确检查error、data及子字段是否存在
  if (error || !data?.targetData) {
    return <div>数据加载失败,请稍后重试</div>;
  }

  // 确保数据存在后再使用相关字段
  return <SvgIcon className={data.targetData.iconClassName} />;
}
  1. 给查询设置默认值或跳过服务端执行:
    如果某些查询在服务端不需要执行,可以设置skip: typeof window === 'undefined';或者在查询定义中添加默认值,避免服务端渲染出空的DOM结构:
// 查询示例
import gql from 'graphql-tag';

const MY_QUERY = gql`
  query MyQuery {
    targetData {
      iconClassName
    }
  }
`;

// 使用时设置默认值
const { data } = useQuery(MY_QUERY, {
  defaultData: { targetData: { iconClassName: 'MuiSvgIcon-root' } }
});

三、全局统一StylesProvider配置

在_app.js中添加StylesProvider,确保客户端和服务端的样式生成逻辑完全一致:

import { StylesProvider } from '@material-ui/styles';
import { ApolloProvider } from '@apollo/react-hooks';
import client from '../apollo/client';

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      {/* 和服务端配置保持一致,设置injectFirst */}
      <StylesProvider injectFirst>
        <Component {...pageProps} />
      </StylesProvider>
    </ApolloProvider>
  );
}

export default MyApp;

四、验证与调试

  • 检查Apollo客户端的缓存配置,确保服务端预取的数据能正确同步到客户端(避免客户端重新查询导致的渲染差异)
  • 打开浏览器控制台,查看是否还有Prop className did not match的警告,如果还有,检查是否有组件依赖window等客户端专属对象生成动态类名
  • 主动模拟查询失败场景,验证UI是否能正常显示错误状态,而非出现超大图标这类异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 23:32:41