Next.js用自定义组件触发Shadcn Dialog解决Hydration错误
解决Next.js中Shadcn Dialog触发器嵌套组件的Hydration错误
问题背景
- 技术栈:Next.js、TypeScript、Tailwind、Shadcn UI
- 需求:将自定义Markdown编辑器组件
<LLEditor>作为Shadcn Dialog的触发器,点击编辑器任意位置打开弹窗
出现的错误
Unhandled Runtime Error Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <button> in <div>. See more info here: https://nextjs.org/docs/messages/react-hydration-error
Unhandled Runtime Error Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
错误原因
Radix UI的DialogTrigger默认会将子元素包裹为<button>元素,但当传入自定义组件(如<LLEditor>)时,服务端渲染的结构和客户端hydration时的结构不匹配,导致Hydration错误。Shadcn的DialogTrigger直接复用了Radix的实现,因此存在同样的问题。
解决方案
提供两种可行修复方式:
方案1:手动控制Dialog显示状态
放弃使用DialogTrigger,通过状态直接控制Dialog的打开/关闭,给<LLEditor>添加点击事件触发状态变更:
"use client" import { useState } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { LLEditor } from "./your-path-to-lleditor" export function EditorDialog() { const [isOpen, setIsOpen] = useState(false) return ( <Dialog open={isOpen} onOpenChange={setIsOpen}> <div onClick={() => setIsOpen(true)}> <LLEditor /> </div> <DialogContent> <DialogHeader> <DialogTitle>What's top of mind for you?</DialogTitle> <DialogDescription> <LLEditor /> </DialogDescription> </DialogHeader> </DialogContent> </Dialog> ) }
方案2:让DialogTrigger渲染为div而非button
利用Radix UIDialogTrigger的asChild属性(Shadcn实现支持该属性),将触发器渲染为<div>,避免自动生成<button>导致结构不匹配:
<Dialog> <DialogTrigger asChild> <div> <LLEditor /> </div> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>What's top of mind for you?</DialogTitle> <DialogDescription> <LLEditor /> </DialogDescription> </DialogHeader> </DialogContent> </Dialog>
注意:如果
<LLEditor>内部有可交互元素(如输入框、按钮),需在这些元素上添加stopPropagation,防止点击时意外打开弹窗:// 在LLEditor内部的可交互元素上添加 <input onClick={(e) => e.stopPropagation()} />
方案2的原理
Radix的DialogTrigger设置asChild后,会将触发逻辑附加到子元素上,而非自动包裹为<button>。用<div>作为容器可确保服务端和客户端渲染结构一致,解决Hydration不匹配问题。
内容的提问来源于stack exchange,提问作者Sina Meraji
相关产品推荐
相关产品推荐

