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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 12:33:13