Sina Meraji
Sina Meraji

Reputation: 101

Triggering a radix dialog (or shadcn dialog) via a React component, not a button

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

Answers (3)

jess benny
jess benny

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

Jasperan
Jasperan

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

ed.tmt
ed.tmt

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

Related Questions