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

React18.2.0+Next.js13.1.5主题切换引发Hydration失败问题求助

Next.js 13 主题切换 Hydration 错误解决办法

问题描述

使用 React 18.2.0 + Next.js 13.1.5 实现点击切换主题功能时,出现两个未处理的运行时错误:

Error: Hydration failed because the initial UI does not match what was rendered on the server
Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering

相关代码:

const [darkMode, setDarkMode] = useState(false);

return (
  <div className={darkMode ? "dark" : ""}>
    <Head>
      <title>Nared Fuengverojsakul</title>
      <meta name="description" content="Generated by create next app" />
      <link rel="icon" href="/favicon.ico" />
    </Head>
    <main className=" bg-white px-10 dark:bg-gray-900 md:px-20 lg:px-40">
      <section className="min-h-screen">
        <nav className="py-10 mb-12 flex justify-between dark:text-white">
          <h1 className="font-burtons text-xl">PORTFOLIO</h1>
          <ul className="flex items-center">
            <li>
              <BsFillMoonStarsFill
                onClick={() => setDarkMode(!darkMode)}
                className=" cursor-pointer text-2xl"
              />
            </li>
          </ul>
        </nav>
      </section>
    </main>
  </div>
);

错误原因

Next.js 采用服务端渲染(SSR),服务端会先生成静态 HTML 返回给客户端,客户端再进行「注水」操作匹配服务端的 DOM 结构。如果客户端初始渲染的 UI 与服务端生成的 HTML 不一致,就会触发 hydration 错误。

你的代码中,服务端初始渲染时 darkMode 为 false,根元素没有 dark 类;但如果客户端尝试从本地存储读取之前保存的主题状态(比如用户之前切换过深色模式),或者在注水过程中立刻修改状态,就会导致客户端与服务端的 DOM 结构不匹配,触发错误。

解决方案

1. 延迟初始化主题状态(推荐)

使用 useEffect 在客户端挂载后再读取本地存储的主题状态,保证服务端与客户端初始渲染的 HTML 一致:

import { useState, useEffect } from 'react';
import Head from 'next/head';
import { BsFillMoonStarsFill } from 'react-icons/bs';

export default function Home() {
  // 服务端初始状态为 false,确保服务端渲染的 HTML 统一
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    // 仅在客户端执行,读取本地存储的主题设置
    const savedDarkMode = localStorage.getItem('darkMode');
    if (savedDarkMode) {
      setDarkMode(JSON.parse(savedDarkMode));
    }
  }, []);

  // 切换主题时同步保存到本地存储
  const toggleTheme = () => {
    const newMode = !darkMode;
    setDarkMode(newMode);
    localStorage.setItem('darkMode', JSON.stringify(newMode));
  };

  return (
    <div className={darkMode ? "dark" : ""}>
      <Head>
        <title>Nared Fuengverojsakul</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className=" bg-white px-10 dark:bg-gray-900 md:px-20 lg:px-40">
        <section className="min-h-screen">
          <nav className="py-10 mb-12 flex justify-between dark:text-white">
            <h1 className="font-burtons text-xl">PORTFOLIO</h1>
            <ul className="flex items-center">
              <li>
                <BsFillMoonStarsFill
                  onClick={toggleTheme}
                  className=" cursor-pointer text-2xl"
                />
              </li>
            </ul>
          </nav>
        </section>
      </main>
    </div>
  );
}

2. 禁用组件的 SSR

使用 Next.js 的 dynamic 导入,让主题切换组件仅在客户端渲染,彻底避免服务端与客户端的状态差异:

// 单独创建 ThemeToggle 组件文件(比如 components/ThemeToggle.js)
import { useState, useEffect } from 'react';
import { BsFillMoonStarsFill } from 'react-icons/bs';

export default function ThemeToggle() {
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    const savedDarkMode = localStorage.getItem('darkMode');
    if (savedDarkMode) {
      setDarkMode(JSON.parse(savedDarkMode));
    }
  }, []);

  const toggleTheme = () => {
    const newMode = !darkMode;
    setDarkMode(newMode);
    localStorage.setItem('darkMode', JSON.stringify(newMode));
    // 同步修改根元素类名
    document.documentElement.classList.toggle('dark', newMode);
  };

  return (
    <BsFillMoonStarsFill
      onClick={toggleTheme}
      className=" cursor-pointer text-2xl"
    />
  );
}

在页面中动态导入并禁用 SSR:

import dynamic from 'next/dynamic';
import Head from 'next/head';

const ThemeToggle = dynamic(() => import('../components/ThemeToggle'), { ssr: false });

export default function Home() {
  return (
    <div>
      <Head>
        <title>Nared Fuengverojsakul</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className=" bg-white px-10 dark:bg-gray-900 md:px-20 lg:px-40">
        <section className="min-h-screen">
          <nav className="py-10 mb-12 flex justify-between dark:text-white">
            <h1 className="font-burtons text-xl">PORTFOLIO</h1>
            <ul className="flex items-center">
              <li>
                <ThemeToggle />
              </li>
            </ul>
          </nav>
        </section>
      </main>
    </div>
  );
}

3. 使用 Suspense 包裹主题组件

用 Suspense 组件包裹主题切换按钮,即使 hydration 出错,也只会影响该组件,不会导致整个应用切换到客户端渲染:

import { useState, useEffect, Suspense } from 'react';
import Head from 'next/head';
import { BsFillMoonStarsFill } from 'react-icons/bs';

export default function Home() {
  const [darkMode, setDarkMode] = useState(false);

  useEffect(() => {
    const savedDarkMode = localStorage.getItem('darkMode');
    if (savedDarkMode) {
      setDarkMode(JSON.parse(savedDarkMode));
    }
  }, []);

  const toggleTheme = () => {
    const newMode = !darkMode;
    setDarkMode(newMode);
    localStorage.setItem('darkMode', JSON.stringify(newMode));
  };

  return (
    <div className={darkMode ? "dark" : ""}>
      <Head>
        <title>Nared Fuengverojsakul</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className=" bg-white px-10 dark:bg-gray-900 md:px-20 lg:px-40">
        <section className="min-h-screen">
          <nav className="py-10 mb-12 flex justify-between dark:text-white">
            <h1 className="font-burtons text-xl">PORTFOLIO</h1>
            <ul className="flex items-center">
              <li>
                <Suspense fallback={<div>Loading...</div>}>
                  <BsFillMoonStarsFill
                    onClick={toggleTheme}
                    className=" cursor-pointer text-2xl"
                  />
                </Suspense>
              </li>
            </ul>
          </nav>
        </section>
      </main>
    </div>
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 08:45:25