Closed3

shadcn/uiのDialogでNextのHydration Warningで怒られる

serinuntiusserinuntius

めちゃ謎な現象に悩まされている

// app/mint/page.tsx

'use client';
import {HogeDialog, Mint} from '@/components/common/collection/mint/Mint';
import {useSWRNFTById} from '@/hooks/firestore/nfts';
import {usePathUtil} from '@/hooks/usePathUtil';
import {useEffect, useState} from 'react';

export default function NftsMint() {
  const {collectionId} = usePathUtil();
  const {nft} = useSWRNFTById(collectionId);
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  return (
    <>
      <h1 className="text-xl font-semibold mb-4">ミント: {nft?.name || ''}</h1>
      {/* {mounted && <HogeDialog />} */}
      <HogeDialog />
    </>
  );
}
// components/common/collection/mint/Mint.tsx

import {useState} from 'react';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import {Button} from '@/components/ui/button';

export const HogeDialog = () => {
  const [isOpen, setIsOpen] = useState(true);

  return (
    <>
      <Dialog open={isOpen} onOpenChange={o => setIsOpen(o)}>
        <DialogContent>
          <DialogTrigger asChild>
            <Button
              onClick={() => {
                setIsOpen(true);
              }}
            >
              実行
            </Button>
          </DialogTrigger>
          <DialogHeader>
            <DialogTitle>関数実行</DialogTitle>
            <DialogDescription>
              引数を設定して、関数を実行します。
            </DialogDescription>
          </DialogHeader>
          aaa
        </DialogContent>
      </Dialog>
    </>
  );
};

これだと

これと

Warning: Expected server HTML to contain a matching <div> in <body>.

これと

Error: Text content does not match server-rendered HTML.
Warning: Expected server HTML to contain a matching <div> in <body>.
See more info here: https://nextjs.org/docs/messages/react-hydration-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.

解決策

// これだとなおる
      {mounted && <HogeDialog />}
      {/* <HogeDialog /> */}
serinuntiusserinuntius

nextのバージョンのバグかと思って、最新にしたり、逆に下げたりしてみたけど変わらず。

turborepoの有無も関係ない。

ちなみにDialogはshadcn/uiで生成されたもの

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 = ({
  className,
  ...props
}: DialogPrimitive.DialogPortalProps) => (
  <DialogPrimitive.Portal className={cn(className)} {...props} />
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;

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-background/80 backdrop-blur-sm 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-lg 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 md:w-full',
        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,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
};

serinuntiusserinuntius

radix-uiのissueに上がってた。デフォルトOpenでtrueにすると起こるらしい・・・・

他のところのDialogでエラーが出ずに、検証用で常にTrueにしてたのが元凶 👿👿👿👿👿👿👿👿👿👿👿👿

The same issue here when working with next.js, any plan to fix this? Now my workaround is set the open to false default, then set it true in useEffect when page has mounted.

https://github.com/radix-ui/primitives/issues/1386#issuecomment-1171798282

まとめ

open={true} 渡すと死ぬぞ

このスクラップは2023/10/21にクローズされました