shadcn/uiのDialogでNextのHydration Warningで怒られる
めちゃ謎な現象に悩まされている
// 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 /> */}
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,
};
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.
まとめ
open={true}
渡すと死ぬぞ