Reputation: 101
I'm building a nextjs app with typescript and tailwind and also using shadcn components.
There's a behaviour I'm trying to create, and for the most part I am able to, but not without hydration errors on the frontend.
The behaviour I'm looking to create:
The errors I get:
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
and
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.
I know why this is happening but don't know how to fix it. The expected input for radix DialogTrigger is a button. The expected input for a shadcn DialogTrigger is just plain text (which then becomes the text on the button). I'm passing a whole component to it. The way shadcn dialog is implemented seems like it should allow it but it doesn't quite in action.
How I'm passing my editor to shadcn dialog:
<Dialog>
<DialogTrigger >
<LLEditor/>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>What's top of mind for you?</DialogTitle>
<DialogDescription>
<LLEditor/>
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
How shadcn dialog is implemented:
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-[60%] translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
How can I achieve my desired behavior?
Thank you so much
Upvotes: 3
Views: 11593
Reputation: 1
Use
<DialogTrigger asChild>
<LLEditor/>
</DialogTrigger>
asChild : Change the default rendered element for the one passed as a child, merging their props and behavior.
Upvotes: 0
Reputation: 3766
Instead of using <LLEditor/>
in DialogTrigger
, you can have a dialog open state
const [isDialogOpen, setIsDialogOpen] = useState(false);
And then do setIsDialogOpen(true)
when <LLEditor>
is clicked. The dialog code would look something like this:
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
Content goes here
</DialogDescription>
</DialogContent>
</Dialog>
You could also try using asChild
like so:
<DialogTrigger asChild>
<LLEditor/>
</DialogTrigger>
Upvotes: 1
Reputation: 31
I would suggest you to manage an open state of dialog, eg. recoil state, react state. So you are not necessarily need to use DialogTrigger to open the dialog, and dialog can be place anywhere instead of nested into your editor.
Upvotes: 0