Reputation: 130
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 :)
Upvotes: 3
Views: 3750
Reputation: 145
I've encountered this exact same problem today, and it was actually much easier than I thought, just merge <SubmitButton/>
into your <AddCandidateModal/>
, then do a useEffect
like this:
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure()
const [state, formAction] = useFormState(serverAction, { message: "" })
React.useEffect(() => {
if (state.message === "ok") {
onClose()
}
}, [state])
Upvotes: 1
Reputation: 151
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]);
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]);
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
Upvotes: 1
Reputation: 49571
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]);
Upvotes: 0