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

Next.js Hydration Error求助:疑因WithAuth高阶组件引发匹配问题

Next.js Hydration错误排查:Expected server HTML to contain a matching
in

问题说明

报错提示:Warning: Expected server HTML to contain a matching <main> in <div>,初步定位问题出在高阶组件WithAuth.js中。

相关代码

导师仪表盘组件(MentorDash.jsx)

import BarChart from "@/components/mentor/chart/bar-chart/page";
import LineChart from "@/components/mentor/chart/line-chart/page";
import Image from "next/image";
import React from "react";
import "./page.scss";
import StdTable from "@/components/mentor/student-table/page";
import HowItWorksSection from "@/components/mentor/how-it-works/page";
import { useSelector } from "react-redux";

const MentorDash = () => {
  const user = useSelector((state) => state.auth.data.user);

  return (
    <main className="col-md-9 ms-sm-auto col-lg-10 p-mb p-0 mentor-profile">
      <div className="bg-screen position-relative">
        <div className="row py-4">
          <div className="col-sm-12">
            <h1 className="text-white">Welcome Back, {user.first_name} {user.last_name}.</h1>
          </div>
        </div>
        <div className="row g-3 py-3">
          <div className="col-sm-6 col-lg-3  ">
            <div className="p-3 bg-white rounded justify-content-between d-flex">
              <span>
                <p className="dashboard-p-label">Students</p>
                <h3>342</h3>
              </span>
              <Image src="/mentor-home/std.svg" height={60} width={60} />
            </div>
          </div>
          <div className="col-sm-6 col-lg-3  ">
            <div className="p-3 bg-white rounded d-flex justify-content-between">
              <span>
                <p className="dashboard-p-label">Awards</p>
                <h3>30+</h3>
              </span>
              <Image src="/mentor-home/award.svg" height={60} width={60} />
            </div>
          </div>
          <div className="col-sm-6 col-lg-3">
            <div className="p-3 bg-white rounded d-flex justify-content-between">
              <span>
                <p className="dashboard-p-label">Staff</p>
                <h3>312+</h3>
              </span>
              <Image src="/mentor-home/staff.svg" height={60} width={60} />
            </div>
          </div>
          <div className="col-sm-6 col-lg-3">
            <div className="p-3 bg-white rounded d-flex justify-content-between">
              <span>
                <p className="dashboard-p-label">Revenue</p>
                <h3>$34M</h3>
              </span>
              <Image src="/mentor-home/revenue.svg" height={60} width={60} />
            </div>
          </div>
        </div>
        <div className="row g-3 pb-3">
          <div className="col-sm-12 col-lg-6 rounded">
            <LineChart />
          </div>
          <div className="col-sm-12 col-lg-6 rounded">
            <BarChart />
          </div>
        </div>
        <div className="row g-1">
          <div className="col-sm-12">
            <StdTable />
          </div>
        </div>
        <HowItWorksSection />
      </div>
    </main>
  );
};

export default MentorDash;

导师主页组件(MentorHome.jsx)

"use client";
import React, { useState } from "react";
import "./page.scss";
import MentorDash from "@/components/mentor/dashboard/page";
import ProfileCompletion from "@/components/mentor/profile/page";
import WithAuth from "@/components/withAuth";

const MentorHome = () => {
  const [displayMentorDash, setDisplayMentorDash] = useState(true);

  const handleSwitchComponent = () => {
    setDisplayMentorDash(!displayMentorDash);
  };

  return (
    <>
      {displayMentorDash ? <MentorDash /> : <ProfileCompletion />}
      <button className="btn btn-lg btn-info" onClick={handleSwitchComponent}>
        Switch Component
      </button>
    </>
  );
};

export default WithAuth(MentorHome);

高阶组件WithAuth.jsx

"use client";
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useRouter, usePathname } from "next/navigation";
import { logoutUser } from "@/app/Redux/slices/authSlice";

export default function WithAuth(Component) {
  return function WithAuth(props) {
    const router = useRouter();
    const pathname = usePathname().split("/");
    const token = useSelector((state) => state.auth.data.accessToken);
    const user = useSelector((state) => state.auth.data.user);
    const dispatch = useDispatch();

    const logoutTimerRef = useRef(null);

    useEffect(() => {
      if (!token) {
        router.push("/");
      }


    useEffect(() => {
      // Redirect logic based on user role and pathname
      if (user == null) {
        router.push("/");
      }
      if (user != null && user.role[0].id == 3 && pathname[1] == "mentor") {
        router.push("/student/home");
      }
      if (user != null && user.role[0].id == 2 && pathname[1] == "student") {
        router.push("/mentor/home");
      }
    }, [pathname, router, user]);

    return token ? <Component {...props} /> : null;
  };
}

问题根源与修复方案

核心原因

服务端渲染时,WithAuth组件因无法获取客户端Redux中的token,返回null;但客户端渲染时返回实际组件,导致服务端和客户端输出的HTML结构完全不匹配——服务端没有<main>标签,客户端却渲染出了<main>,触发Hydration不匹配错误。此外,WithAuth中第一个useEffect未闭合,存在语法错误,也会加剧渲染异常。

修复步骤

  1. 统一服务端与客户端根元素:服务端渲染阶段返回空的占位元素(如<div>),而非null,保证两端HTML结构有匹配的根节点。
  2. 修复语法错误:闭合未结束的useEffect。
  3. 优化状态判断逻辑:添加客户端就绪标记,避免Hydrate时状态不一致。

修复后的WithAuth.jsx

"use client";
import { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useRouter, usePathname } from "next/navigation";
import { logoutUser } from "@/app/Redux/slices/authSlice";

export default function WithAuth(Component) {
  return function WithAuth(props) {
    const router = useRouter();
    const pathname = usePathname().split("/");
    const token = useSelector((state) => state.auth.data.accessToken);
    const user = useSelector((state) => state.auth.data.user);
    const dispatch = useDispatch();
    const [isClientReady, setIsClientReady] = useState(false);

    const logoutTimerRef = useRef(null);

    useEffect(() => {
      // 标记客户端已初始化,避免服务端/客户端渲染差异
      setIsClientReady(true);
      
      if (!token) {
        router.push("/");
      }
    }, [token, router]);

    useEffect(() => {
      // 角色与路径匹配的重定向逻辑
      if (!user) {
        router.push("/");
      }
      if (user && user.role[0].id === 3 && pathname[1] === "mentor") {
        router.push("/student/home");
      }
      if (user && user.role[0].id === 2 && pathname[1] === "student") {
        router.push("/mentor/home");
      }
    }, [pathname, router, user]);

    // 服务端返回占位div,客户端渲染实际组件
    if (!isClientReady) {
      return <div></div>;
    }

    return token ? <Component {...props} /> : <div></div>;
  };
}

额外检查项

  • 给MentorDash中的user添加可选链:user?.first_name,避免user未初始化时的渲染错误。
  • 确认ProfileCompletion组件也返回稳定的根元素,避免切换组件时的结构差异。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 07:32:07