React 18下Next Link结合自定义Button组件出现Hydration不匹配错误
问题现象
在React 18环境下,使用next/link结合自定义Button组件实现带按钮样式的锚点链接时,触发以下错误:
Hydration failed because the initial UI does not match what was rendered on the server.
控制台同时提示:
Expected server HTML to contain a matching
注释相关链接后错误消失,确认问题源于链接渲染逻辑,且该组件在React 17中可正常运行。
自定义Button组件代码
const variants = { primary: "btn-primary", secondary: "btn-secondary", delete: "btn-red", white: "btn-white", none: "", }; type IconProps = | { startIcon: React.ReactElement; endIcon?: never } | { endIcon: React.ReactElement; startIcon?: never } | { endIcon?: undefined; startIcon?: undefined }; export type ButtonProps = React.ButtonHTMLAttributes< HTMLButtonElement | HTMLAnchorElement > & { variant?: keyof typeof variants; isLoading?: boolean; asAnchor?: boolean; } & IconProps; export const Button = React.forwardRef< HTMLButtonElement | HTMLAnchorElement, ButtonProps >( ( { type = "button", className = "", variant = "primary", isLoading = false, startIcon, endIcon, asAnchor, disabled, ...props }, ref ) => { return ( <> {asAnchor ? ( <a ref={ref} className={clsx( className, variants[variant], "appearance-none", disabled && "pointer-events-none opacity-90" )} {...props} > {!isLoading && startIcon} {!isLoading && props.children} {!isLoading && endIcon} </a> ) : ( <button ref={ref} type={type} className={clsx( className, variants[variant], "disabled:pointer-events-none disabled:opacity-60" )} disabled={disabled} {...props} > {isLoading && ( <span className="flex items-center justify-center space-x-2"> <Spinner /> <span>Loading</span> </span> )} {!isLoading && startIcon} {!isLoading && <> {props.children} </>} {!isLoading && endIcon} </button> )} </> ); } ); Button.displayName = "Button";
组件使用方式
<Link passHref href="/login"> <Button className="text-xs" asAnchor> Login </Button> </Link>
问题原因分析
- React 18 Hydration校验更严格:React 18对服务端渲染(SSR)的DOM结构一致性要求更高,细微差异都会触发hydration错误。
- 无效属性传递:当
asAnchor为true时,原代码将Button的disabled属性直接传递给<a>标签,但<a>原生不支持该属性,导致服务端渲染的HTML包含无效属性,客户端hydration时无法匹配。 - Ref未正确传递:原代码中
<a>标签未接收forwardRef传递的ref,可能导致DOM节点引用不一致。 - 条件渲染潜在差异:如果
isLoading状态在服务端和客户端初始值不同,会导致startIcon/endIcon的渲染结果不一致,触发DOM不匹配。
解决方案
- 过滤无效属性:将
disabled从传递给<a>的props中分离,仅通过className控制禁用状态,避免给<a>添加原生不支持的属性。 - 传递Ref到目标元素:确保
forwardRef的ref正确传递给<a>或<button>元素。 - 保证状态一致性:确保
isLoading等状态在服务端和客户端的初始值一致,避免条件渲染导致的DOM差异。 - 优化Next.js Link使用:确保
passHref正确传递href属性到Button组件内的<a>标签。
修改后的关键代码说明
- 从props中解构出
disabled,不再传递给<a>标签,仅通过clsx添加禁用样式。 - 给
<a>和<button>标签都添加ref={ref},确保ref正确传递。 - 保持条件渲染逻辑的一致性,确保服务端和客户端渲染的内容一致。
内容的提问来源于stack exchange,提问作者squish
相关产品推荐
相关产品推荐

