Published on

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

Authors
  • Name
    Ripal & Zalak
    Twitter

When working with Radix or Shadcn dialogs in React, especially in a Next.js project, triggering the dialog using a custom component instead of a button can lead to hydration errors. This guide explains how to implement such functionality while avoiding these issues.

The Problem

Suppose you want to use a custom component, such as a markdown text editor, to trigger a dialog. Here’s an example scenario:

  • Clicking on a custom component opens a dialog.
  • The custom component is passed as the DialogTrigger.

Example Code:

<Dialog>
  <DialogTrigger>
    <LLEditor />
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>What's top of mind for you?</DialogTitle>
      <DialogDescription>
        <LLEditor />
      </DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>

This results in hydration errors such as:

  1. Error: Hydration failed because the initial UI does not match what was rendered on the server.
  2. Warning: Expected server HTML to contain a matching <button> in <div>.

The issue arises because Radix or Shadcn DialogTrigger expects a specific DOM structure, such as a button, which conflicts with the custom component.

Solution 1: Use asChild

The asChild prop allows you to use a custom component as the trigger without Radix adding additional DOM elements. Here’s how:

<Dialog>
  <DialogTrigger asChild>
    <LLEditor />
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>What's top of mind for you?</DialogTitle>
      <DialogDescription>Content goes here</DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>

Why This Works

The asChild prop ensures that DialogTrigger renders its child directly without wrapping it in additional DOM elements. This eliminates the hydration mismatch.

Solution 2: Manage Open State Manually

Instead of relying on DialogTrigger, you can manage the dialog's open state explicitly. Here’s an example:

const [isDialogOpen, setIsDialogOpen] = useState(false)

return (
  <>
    <LLEditor onClick={() => setIsDialogOpen(true)} />
    <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>What's top of mind for you?</DialogTitle>
          <DialogDescription>Content goes here</DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  </>
)

Benefits of This Approach

  • Provides full control over when the dialog opens.
  • Decouples dialog logic from the trigger, allowing greater flexibility.

Additional Tips

  1. Avoid Nesting Triggers: Ensure the trigger component does not contain elements that might also act as triggers, which can confuse Radix's internal logic.
  2. Test for Accessibility: If you’re bypassing the default button, ensure the custom trigger is accessible (e.g., keyboard navigable, screen-reader friendly).
  3. Inspect DOM: Use browser developer tools to confirm that no unexpected elements are being rendered around the trigger.

Conclusion

Triggering Radix or Shadcn dialogs with custom React components is straightforward when using the asChild prop or managing the dialog state manually. These approaches prevent hydration errors and ensure smooth, predictable behavior. By following these practices, you can create intuitive and error-free user interfaces in your Next.js projects.