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; }
错误根源
- 双重空渲染导致DOM差异:页面组件和高阶组件都通过
mounted状态在初始渲染时返回null,服务器渲染输出空内容,但客户端hydrate完成后会直接渲染目标组件或无权限组件,前后DOM结构完全不匹配。 - 权限判断依赖客户端状态:
useStore获取用户角色是客户端专属状态,服务器渲染阶段无法获取用户信息,导致服务器输出空DOM,客户端渲染时根据权限返回不同组件,触发Hydration校验失败。 - 无权限组件结构不匹配:
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
相关产品推荐
相关产品推荐

