添加Prism line-numbers类引发Next.js React Hydration失败问题
Next.js中PrismJS行号插件导致Hydration失败的解决方案
问题描述
使用Next.js、GraphCMS和TailwindCSS开发时,为<pre>标签添加line-numbers类以实现PrismJS行号样式,触发React Hydration失败错误;移除该类名后代码正常运行,但无法显示带行号的代码块。相关代码如下:
import { GraphQLClient, gql } from "graphql-request"; import { RichText } from "@graphcms/rich-text-react-renderer"; import { useEffect } from "react"; import Prism from "prismjs"; import "prismjs/plugins/line-numbers/prism-line-numbers"; import "prismjs/themes/prism-tomorrow.css"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; export default function Demo({ posts }) { useEffect(() => { Prism.highlightAll(); }, []); return ( <section className="prose m-auto"> <h1>{posts.title}</h1> <RichText content={posts.content.json} renderers={{ code_block: ({ children }) => { return ( <pre className="line-numbers language-none"> <code>{children}</code> </pre> ); }, }} /> </section> ); } export const getServerSideProps = async (context) => { const endPoint = "https://randomenpoint.com/api"; const slug = "random slug"; const query = gql` query ($slug: String!) { posts(where: { slug: $slug }) { id publishedAt createdAt slug title updatedAt content { json } } } `; const client = new GraphQLClient(endPoint); const { posts } = await client.request(query, { slug }); return { props: { posts, }, }; };
问题根源
Hydration失败的核心原因是服务端渲染的DOM结构与客户端Hydration时生成的DOM结构不一致。PrismJS行号插件会在客户端执行时,为<pre>元素添加额外的DOM节点(如包裹行号的<span>元素),但服务端渲染阶段这些节点不存在,导致客户端Hydration时DOM比对不匹配,触发错误。
解决方法
方法一:环境判断+状态控制
通过环境判断确保PrismJS行号资源仅在客户端加载,并用状态控制line-numbers类的添加时机,保证服务端与客户端初始DOM结构一致:
import { GraphQLClient, gql } from "graphql-request"; import { RichText } from "@graphcms/rich-text-react-renderer"; import { useEffect, useState } from "react"; // 仅在客户端环境加载PrismJS行号相关资源 let Prism; if (typeof window !== 'undefined') { Prism = require("prismjs"); require("prismjs/plugins/line-numbers/prism-line-numbers"); require("prismjs/themes/prism-tomorrow.css"); require("prismjs/plugins/line-numbers/prism-line-numbers.css"); } export default function Demo({ posts }) { // 标记是否已完成客户端Hydration const [isClient, setIsClient] = useState(false); useEffect(() => { // Hydration完成后更新状态并执行Prism高亮 setIsClient(true); Prism?.highlightAll(); }, []); return ( <section className="prose m-auto"> <h1>{posts.title}</h1> <RichText content={posts.content.json} renderers={{ code_block: ({ children }) => { // 仅在客户端添加line-numbers类 return ( <pre className={`${isClient ? 'line-numbers' : ''} language-none`}> <code>{children}</code> </pre> ); }, }} /> </section> ); } // getServerSideProps代码保持不变 export const getServerSideProps = async (context) => { const endPoint = "https://randomenpoint.com/api"; const slug = "random slug"; const query = gql` query ($slug: String!) { posts(where: { slug: $slug }) { id publishedAt createdAt slug title updatedAt content { json } } } `; const client = new GraphQLClient(endPoint); const { posts } = await client.request(query, { slug }); return { props: { posts, }, }; };
方法二:动态导入客户端组件
使用Next.js的dynamic组件,将代码块渲染逻辑标记为仅客户端执行,彻底避免服务端与客户端的DOM结构冲突:
- 创建单独的代码块组件
CodeBlock.js:
import { useEffect } from "react"; import Prism from "prismjs"; import "prismjs/plugins/line-numbers/prism-line-numbers"; import "prismjs/themes/prism-tomorrow.css"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; export default function CodeBlock({ children }) { useEffect(() => { Prism.highlightAll(); }, []); return ( <pre className="line-numbers language-none"> <code>{children}</code> </pre> ); }
- 在页面组件中动态导入该组件:
import { GraphQLClient, gql } from "graphql-request"; import { RichText } from "@graphcms/rich-text-react-renderer"; import dynamic from "next/dynamic"; // 禁用服务端渲染,仅在客户端加载CodeBlock组件 const CodeBlock = dynamic(() => import("./CodeBlock"), { ssr: false }); export default function Demo({ posts }) { return ( <section className="prose m-auto"> <h1>{posts.title}</h1> <RichText content={posts.content.json} renderers={{ code_block: ({ children }) => <CodeBlock children={children} />, }} /> </section> ); } // getServerSideProps代码保持不变 export const getServerSideProps = async (context) => { const endPoint = "https://randomenpoint.com/api"; const slug = "random slug"; const query = gql` query ($slug: String!) { posts(where: { slug: $slug }) { id publishedAt createdAt slug title updatedAt content { json } } } `; const client = new GraphQLClient(endPoint); const { posts } = await client.request(query, { slug }); return { props: { posts, }, }; };
说明
两种方法核心思路都是让服务端与客户端初始渲染的DOM结构保持一致,避免PrismJS行号插件在客户端修改DOM导致的Hydration冲突。方法二更清晰,适合复杂的代码块渲染逻辑;方法一更轻量,适合简单场景。
内容的提问来源于stack exchange,提问作者Amit Kumar
相关产品推荐
相关产品推荐

