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未闭合,存在语法错误,也会加剧渲染异常。
修复步骤
- 统一服务端与客户端根元素:服务端渲染阶段返回空的占位元素(如
<div>),而非null,保证两端HTML结构有匹配的根节点。 - 修复语法错误:闭合未结束的
useEffect。 - 优化状态判断逻辑:添加客户端就绪标记,避免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
相关产品推荐
相关产品推荐

