Search code examples
javascriptreactjsnext.jsreact-hooksshadcnui

Can not change state from Dialog button onClick event


I am working on a Next.js app, and in one of the components, I have a DropdownMenu where one of its items launches a DialogAlert (both are Shadcn components). I am trying to control the dialog's open state with setIsDialogOpen, but it only works when there is a synchronous function before it (like the confirm button example). Otherwise, it won't work when I simply call setIsDialogOpen(false) from the cancel button action.

Here is the code to reproduce the problem:

"use client";
import { useState } from "react";
import {
 AlertDialog,
 AlertDialogAction,
 AlertDialogCancel,
 AlertDialogContent,
 AlertDialogDescription,
 AlertDialogFooter,
 AlertDialogHeader,
 AlertDialogTitle,
 AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
 DropdownMenu,
 DropdownMenuContent,
 DropdownMenuItem,
 DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Loader2 } from "lucide-react";

export default function Home() {
 const [isDialogOpen, setIsDialogOpen] = useState(false);
 const [isLoading, setIsLoading] = useState(false);

 const handleCancelDelete = () => {
   setIsDialogOpen(false);
 };

 const handleDelete = async () => {
   setIsLoading(true);
   // Perform your delete operation here
   await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating an async operation
   setIsLoading(false);
 };

 return (
   <div>
     <DropdownMenu>
       <DropdownMenuTrigger>Dropdown</DropdownMenuTrigger>
       <DropdownMenuContent>
         <DropdownMenuItem>Action</DropdownMenuItem>
         <DropdownMenuItem
           onClick={() => setIsDialogOpen(true)}
           onSelect={(event) => event.preventDefault()}
         >
           <AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
             <AlertDialogTrigger>Delete</AlertDialogTrigger>
             <AlertDialogContent>
               <AlertDialogHeader>
                 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
               </AlertDialogHeader>
               <AlertDialogFooter>
                 <AlertDialogCancel
                   onClick={() => {
                     alert("onClick works");
                     setIsDialogOpen(false);
                   }}
                 >
                   Cancel
                 </AlertDialogCancel>
                 {isLoading ? (
                   <Loader2 className="animate-spin" />
                 ) : (
                   <AlertDialogAction
                     onClick={async (e) => {
                       e.preventDefault(); // Prevent default action that closes the dialog
                       await handleDelete();
                       setIsDialogOpen(false);
                     }}
                   >
                     Continue
                   </AlertDialogAction>
                 )}
               </AlertDialogFooter>
             </AlertDialogContent>
           </AlertDialog>
         </DropdownMenuItem>
       </DropdownMenuContent>
     </DropdownMenu>
   </div>
 );
}

Here is a CodeSandbox link to reproduce the issue: https://codesandbox.io/p/devbox/cranky-montalcini-4432z5?embed=1&file=%2Fapp%2Fpage.tsx%3A38%2C20


Solution

  • Maybe you also wants to close the dropdown on press of cancel button in alert dialog. To achieve this you need to convert the dropdown component from uncontrolled to controlled component by passing open and onChangeOpen props to dropdown component.

    export default function Home() {
      const [isDialogOpen, setIsDialogOpen] = useState(false);
      const [isLoading, setIsLoading] = useState(false);
    
      const handleCancelDelete = () => {
        setIsDialogOpen(false);
      };
    
      const handleDelete = async () => {
        setIsLoading(true);
        // Perform your delete operation here
        await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating an async operation
        setIsLoading(false);
      };
    
      const [isDropDownOpen, setIsDropDownOpen] = useState(false);
    
      return (  
        <div> 
          <DropdownMenu open={isDropDownOpen} onOpenChange={() => {
            setIsDropDownOpen(!isDropDownOpen);
          }}>
            <DropdownMenuTrigger>Dropdown</DropdownMenuTrigger>
            <DropdownMenuContent>
              <DropdownMenuItem>Action</DropdownMenuItem>
              <DropdownMenuItem
                onClick={() => setIsDialogOpen(true)}
                onSelect={(event) => event.preventDefault()}
              >
                <AlertDialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
                  <AlertDialogTrigger>Delete</AlertDialogTrigger>
                  <AlertDialogContent>
                    <AlertDialogHeader>
                      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
                    </AlertDialogHeader>
                    <AlertDialogFooter>
                      <AlertDialogCancel
                        onClick={(e) => {
                          e.stopPropagation(); // Add this line to stop event propagation
                          setIsDropDownOpen(false);
                          setIsDialogOpen(false);
                        }}
                      >
                        Cancel
                      </AlertDialogCancel>
                      {isLoading ? (
                        <Loader2 className="animate-spin" />
                      ) : (
                        <AlertDialogAction
                          onClick={async (e) => {
                            e.preventDefault(); // Prevent default action that closes the dialog
                            await handleDelete();
                            setIsDialogOpen(false);
                          }}
                        >
                          Continue
                        </AlertDialogAction>
                      )}
                    </AlertDialogFooter>
                  </AlertDialogContent>
                </AlertDialog>
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
      );
    }