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警告,同时放大样式不匹配的问题(比如图标没有正确的类名)。你需要做防御性渲染:
- 在组件中明确检查数据存在性再渲染:
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} />; }
- 给查询设置默认值或跳过服务端执行:
如果某些查询在服务端不需要执行,可以设置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
相关产品推荐
相关产品推荐

