Close modal after form action is successful

790 Views Asked by At

I am using @nextjs ~ 14.0.1 and next-ui. I have a modal component where I have a form and I am using form action. Here's the modal component and action function's code ~

AddCandiateModal.tsx ⤵️

import React from "react";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure, Input, Code, Select, SelectItem } from "@nextui-org/react";
import { PlusIcon } from "./PlusIcon";
import { useFormState, useFormStatus } from "react-dom";
import { useForm } from "react-hook-form";
import { createCandidateAction } from "@/app/actions/candidate-action";
import { CandidateStatusType } from "@/types/campaign";


function SubmitButton() {
    const { pending } = useFormStatus()
    return (
        <Button color="primary" aria-disabled={pending} type="submit" isLoading={pending}>Save</Button>
    )
}

export default function AddCandidateModal({ campaign_id, status }: { campaign_id: number, status: CandidateStatusType[] }) {
    const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
    const { register } = useForm()
    const [state, formAction] = useFormState(createCandidateAction, null)
    return (
        <>
            <Button onPress={onOpen} color="primary" endContent={<PlusIcon />}>Add New</Button>
            <Modal isOpen={isOpen}  onOpenChange={onOpenChange}>
                <ModalContent>
                    {(onClose) => (
                        <>
                            <form 
                                action={formAction}
                                
                            >
                                <ModalHeader className="flex flex-col gap-1">New Candidate</ModalHeader>
                                <ModalBody>
                                    <Select
                                        items={status}
                                        label="Candidate Status"
                                        placeholder="Select a status"
                                        className=""
                                        isRequired
                                        {...register('status')}
                                    >
                                        {(animal) => <SelectItem key={animal.id}>{animal.name}</SelectItem>}
                                    </Select>
                                    <div className="grid grid-cols-2 gap-3">
                                        <Input isRequired label="First Name" placeholder="Travis" {...register('first_name')} />
                                        <Input isRequired label="Last Name" placeholder="Scott" {...register('last_name')} />
                                    </div>
                                    <Input isRequired type="email" label="Email" placeholder="[email protected]" {...register('email')} />
                                    <Input isRequired type="phone" label="Phone" placeholder="123 45678" {...register('phone_number')} />
                                    {/* <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white" htmlFor="file_input">Upload Resume</label> */}
                                    <span className="text-gray-500">Upload Resume</span>
                                    <input className=" w-full py-3 px-3 text-gray-500 rounded-lg cursor-pointer bg-gray-100 " type="file" />
                                    <input type="hidden" value={campaign_id} {...register("campaign")} />
                                </ModalBody>
                                <ModalFooter>
                                    {state?.message && <Code>{state.message}</Code>}
                                    <Button color="danger" variant="light" onPress={onClose}>
                                        Close
                                    </Button>
                                    <SubmitButton />
                                </ModalFooter>
                            </form>
                        </>
                    )}
                </ModalContent>
            </Modal>
        </>
    );
}

candiate-action.ts ⤵️

'use server';

import { revalidatePath } from "next/cache";
import authSession from "@/utils/authsession";
import { redirect } from "next/navigation";


export async function createCandidateAction(prevState: any, formData: FormData ) {
    const session = await authSession();
    const campaign_id = parseInt(formData.get('campaign') as string);
    console.log(prevState);
    const data = {
        campaign: formData.get('campaign'),
        first_name: formData.get('first_name'),
        last_name: formData.get('last_name'),
        email: formData.get('email'),
        phone_number: formData.get('phone_number'),
        status: parseInt(formData.get('status') as string),
    }
    const response = await fetch(`${process.env.NEXTAUTH_BACKEND_URL}campaigns/${campaign_id}/candidate/`, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Token ${session.key}`,
        }
    });
    if (response.ok) {
        const candidate = await response.json();
        revalidatePath(`/campaigns/${campaign_id}/candidates/`);
        // redirect(`/campaigns/${campaign_id}/candidates/`);
        return candidate;
    } else {
        const error = await response.json();
        return {
            message: error.detail,
        };
    }
}

I can't pass the onClose to the server component.

If I redirect from the action, the modal is still opened. Now, what should I do to close this modal? Thanks :)

2

There are 2 best solutions below

1
On

you are using a specific library and its functions. we need to test the library to see the specific issue. if you track the open state with useState

const [open, setOpen] = useState(false);

you are already using useFormState. inside useEffect

useEffect(() => {
    if (formState.success) {
      // apply logic here
      setIsopen(false)
    }
  }, [formState]);
0
On

The problem is that, as @ProEvilz mentioned, the formState.success will always be true when you submit two successful submissions in a row. That means by doing what @Yilmaz did above, your modal will close successfully on the first submission, but will not on the second.

useEffect(() => {
  if (state.success) {
    toast.success(state.message);
    onClose(); // function to close your modal
  }
}, [state.success]);

Solution 1: A unique resetKey

Since state.success will be true on two successful submissions in a row, the effect will not get triggered since it will not change on the second submission. To retrigger this each time, we can return a unique resetKey from the server action on each successful response like this:

'use server';
//...     
return {
    success: true,
    message: "Menu category created",
    resetKey: Date.now().toString(), // can be anything, but we use the date
};

And then change our useEffect() to the following:

  useEffect(() => {
    if (state.success) {
      toast.success(state.message);
      onClose();
    }
  }, [state.resetKey, state.success]);

Solution 2: useTransition()

Since you're using a modal, there really isn't any point in using formState. Why? Because the modal requires JavaScript to open/close making progressive enhancement not possible. If JS is disabled, there will be no way to view the form anyways. So why not just call the form action from a function?

const [loading, setTransitioning] = useTransition();
// I'm using React-Hook-Form here, but the idea is the same without it.
const onSubmit = methods.handleSubmit((data) => {
  setTransitioning(async () => {
    const res = await editRestaurant(data);
    if (res.success === false) {
      toast.error(res.message);
    } else {
      toast.success("Restaurant updated successfully");
      onClose() // Close modal here
    }
  });
});
//...
<form onSubmit={onSubmit} />

We use useTransition() over a regular async function because:

useTransition is a React Hook that lets you update the state without blocking the UI.

If you would still like to use a modal with progressive enhancement, you should be using query parameters instead, such as example.com/test?modal=true and then redirect the user after a successful submission to example.com/test?modal=false