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

Next.js13.4 + AntD v5角色权限校验时出现Hydration错误求助

解决Next.js 13.4 + Ant Design v5路由权限校验的Hydration不匹配错误

问题概述

在/dashboard/news客户端路由页面中,通过自定义withRoles高阶组件实现NewsAdmin角色权限校验时,触发Hydration错误:

未处理的运行时错误
错误:Hydration失败,因为初始UI与服务器渲染的内容不匹配。
警告:期望服务器HTML中包含匹配的

  • 标签。

  • 相关代码如下:

    页面组件代码

    'use client';
    import React, { useEffect, useState } from 'react';
    import NewsComponent from '@/app/components/news/NewsComponent';
    import { withRoles } from '@/app/libs/permissions/pagesPermissions';
    
    const NewsListPage = () => {
      const [mounted, setMounted] = useState(false);
      useEffect(() => {
        setMounted(true);
      }, []);
      if (!mounted) {
        return null;
      }
      return (
        <>
          <title>اخبار</title>
          <div className={'font-IRANSansXV sm:mx-4 overflow-auto h-[89vh] '}>
            <div className={'flex justify-between items-center'}>
              <h1 className={'font-bold text-lg mt-6 mb-10 px-6 md:mb-6'}>اخبار</h1>
            </div>
            <NewsComponent />
          </div>
        </>
      );
    };
    
    const NewsListPageWithRoles = withRoles(NewsListPage, ['NewsAdmin']);
    export default NewsListPageWithRoles;
    

    权限高阶组件代码

    'use client';
    import React, { useEffect, useState } from 'react';
    import { useStore } from '@/app/store';
    import InaccessibilityComponent from '@/app/components/InaccessibilityComponent';
    
    export const hasPermissionForRoles = (
      requiredRoles: string[],
      userRoles: string[],
    ): boolean => {
      return requiredRoles.some((requiredRole) => userRoles.includes(requiredRole));
    };
    
    interface WithRolesWrapperProps {
      // 定义额外props
    }
    
    export function withRoles<P>(
      Component: React.ComponentType<P>,
      requiredPermissions: string[],
    ) {
      const WithRolesWrapper: React.FC<WithRolesWrapperProps & P> = (props) => {
        const [mounted, setMounted] = useState(false);
        const { user } = useStore((state) => state);
        useEffect(() => {
          setMounted(true);
        }, []);
    
        if (!mounted) {
          return null;
        }
    
        const userPermissions = user?.roles;
        const hasPermission = hasPermissionForRoles(
          userPermissions,
          requiredPermissions,
        );
        if (hasPermission) {
          return <Component {...props} />;
        } else {
          return <InaccessibilityComponent />;
        }
      };
    
      return WithRolesWrapper;
    }
    

    错误根源

    1. 双重空渲染导致DOM差异:页面组件和高阶组件都通过mounted状态在初始渲染时返回null,服务器渲染输出空内容,但客户端hydrate完成后会直接渲染目标组件或无权限组件,前后DOM结构完全不匹配。
    2. 权限判断依赖客户端状态:useStore获取用户角色是客户端专属状态,服务器渲染阶段无法获取用户信息,导致服务器输出空DOM,客户端渲染时根据权限返回不同组件,触发Hydration校验失败。
    3. 无权限组件结构不匹配:InaccessibilityComponent的DOM结构与目标页面组件差异过大,进一步加剧渲染不一致问题。

    修复方案

    1. 移除页面组件的冗余mounted判断

    页面组件无需单独处理客户端渲染逻辑,交给高阶组件统一管理:

    'use client';
    import React from 'react';
    import NewsComponent from '@/app/components/news/NewsComponent';
    import { withRoles } from '@/app/libs/permissions/pagesPermissions';
    
    const NewsListPage = () => {
      return (
        <>
          <title>اخبار</title>
          <div className={'font-IRANSansXV sm:mx-4 overflow-auto h-[89vh] '}>
            <div className={'flex justify-between items-center'}>
              <h1 className={'font-bold text-lg mt-6 mb-10 px-6 md:mb-6'}>اخبار</h1>
            </div>
            <NewsComponent />
          </div>
        </>
      );
    };
    
    const NewsListPageWithRoles = withRoles(NewsListPage, ['NewsAdmin']);
    export default NewsListPageWithRoles;
    

    2. 优化高阶组件的渲染一致性

    将初始空渲染改为返回与目标组件结构近似的占位容器,确保服务器和客户端初始渲染的DOM结构一致,同时确保权限判断在客户端完全就绪后执行:

    'use client';
    import React, { useEffect, useState } from 'react';
    import { useStore } from '@/app/store';
    import InaccessibilityComponent from '@/app/components/InaccessibilityComponent';
    
    export const hasPermissionForRoles = (
      requiredRoles: string[],
      userRoles: string[] = [],
    ): boolean => {
      return requiredRoles.some((requiredRole) => userRoles.includes(requiredRole));
    };
    
    interface WithRolesWrapperProps {}
    
    export function withRoles<P>(
      Component: React.ComponentType<P>,
      requiredPermissions: string[],
    ) {
      const WithRolesWrapper: React.FC<WithRolesWrapperProps & P> = (props) => {
        const [isClientReady, setIsClientReady] = useState(false);
        const { user } = useStore((state) => state);
    
        useEffect(() => {
          // 客户端挂载完成后再进行权限判断
          setIsClientReady(true);
        }, []);
    
        // 初始渲染返回与页面组件一致的外层容器,保证DOM结构匹配
        if (!isClientReady) {
          return <div className={'font-IRANSansXV sm:mx-4 overflow-auto h-[89vh] '}></div>;
        }
    
        const userPermissions = user?.roles || [];
        const hasPermission = hasPermissionForRoles(requiredPermissions, userPermissions);
        
        return hasPermission ? <Component {...props} /> : <InaccessibilityComponent />;
      };
    
      return WithRolesWrapper;
    }
    

    3. 对齐无权限组件的DOM结构

    确保InaccessibilityComponent的外层容器与页面组件的外层容器结构一致,比如使用相同的className和层级:

    // 示例InaccessibilityComponent
    const InaccessibilityComponent = () => {
      return (
        <div className={'font-IRANSansXV sm:mx-4 overflow-auto h-[89vh] '}>
          <div className="flex justify-center items-center h-full">
            <h2 className="text-red-500 font-bold">无访问权限</h2>
          </div>
        </div>
      );
    };
    
    export default InaccessibilityComponent;
    

    可选:服务器端权限预校验(进阶)

    如果你的用户权限可以在服务器端获取(比如通过cookie解析用户信息),可以将页面改为服务器组件,提前在服务器端判断权限,从根源避免Hydration问题:

    // 移除'use client',改为服务器组件
    import NewsComponent from '@/app/components/news/NewsComponent';
    import InaccessibilityComponent from '@/app/components/InaccessibilityComponent';
    import { getServerSession } from 'next-auth/next';
    import { authOptions } from '@/app/api/auth/[...nextauth]/route';
    import { Suspense } from 'react';
    
    export default async function NewsListPage() {
      const session = await getServerSession(authOptions);
      const hasPermission = session?.user?.roles?.includes('NewsAdmin');
    
      if (!hasPermission) {
        return <InaccessibilityComponent />;
      }
    
      return (
        <>
          <title>اخبار</title>
          <div className={'font-IRANSansXV sm:mx-4 overflow-auto h-[89vh] '}>
            <div className={'flex justify-between items-center'}>
              <h1 className={'font-bold text-lg mt-6 mb-10 px-6 md:mb-6'}>اخبار</h1>
            </div>
            {/* 如果NewsComponent是客户端组件,用Suspense包裹 */}
            <Suspense fallback={<div>加载中...</div>}>
              <NewsComponent />
            </Suspense>
          </div>
        </>
      );
    }
    

    内容的提问来源于stack exchange,提问作者ahmad ghasemi

    相关产品推荐
    方舟 Agent Plan

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

    最近更新时间:2026.07.12 17:55:59