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

添加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结构冲突:

  1. 创建单独的代码块组件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>
  );
}
  1. 在页面组件中动态导入该组件:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 11:05:31