Managing Form Submissions in a Next.js Project with useFormState and useFormStatus
By O. Wolfson
Handling form submissions in modern web applications is crucial for providing users with a responsive and clear experience. In this article, we’ll build a contact form in a React / Next.js project. We use Supabase for backend operations, but this can be adapted to any datasource of course. Our form will display a loading state during submission to prevent multiple submissions, and reset the form after a successful submission.
Displays a "Submitting..." message while processing.
Prevents multiple submissions.
Updates the UI with a success message once the form is submitted.
Let’s break this down into simple and reusable code examples.
Setting Up the Contact Form
We’ll start by creating the form that users will fill out. In this example, we’re using the useFormState and useFormStatus hooks from React to handle form state and submission status. Here’s the code for the form:
useFormState: Initializes and manages the form’s state. It triggers the sendContactMessage action when the form is submitted.
useFormStatus: Tracks the submission status, allowing us to display "Submitting..." and disable the button while the form is being processed.
SubmitButton Component: Handles the button state and updates its label based on the submission status.
Handling the Form Submission
Now let’s implement the sendContactMessage action, which submits the form data to Supabase (or any backend of your choice). This example saves the contact message in a Supabase table called contact_test_app.
javascript
"use server";
import { revalidatePath } from"next/cache";
import { z } from"zod";
import { createClient } from"@/lib/supabase/supabase-client-server";
import { redirect } from"next/navigation";
exportasyncfunctionsendContactMessage(
state: { message: string },
formData: FormData
) {
const supabase = createClient();
// Define schema for validationconst schema = z.object({
name: z.string(),
email: z.string().email(),
message: z.string(),
type: z.string(),
});
// Parse and validate the form dataconst data = schema.parse({
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
type: formData.get("type"),
});
// Simulate an async taskawaitnewPromise((resolve) =>setTimeout(resolve, 500));
// Insert data into Supabaseconst { error } = await supabase.from("contact_test_app").insert([
{
name: data.name,
email: data.email,
message: data.message,
type: data.type,
created_at: newDate(), // Optional timestamp
},
]);
if (error) {
console.error("Error inserting data:", error);
return {
message: "An error occurred while sending your message.",
};
}
// Revalidate the page and redirect to a thank you pagerevalidatePath("/contact");
redirect("/contact/thank-you");
return {
message: "Your message has been sent successfully.",
};
}
Explanation:
Data Validation: Using zod, we validate the input to ensure that all fields are present and formatted correctly.
Supabase Integration: We insert the validated data into Supabase. If any errors occur during insertion, we return an error message.
Revalidate and Redirect: After the data is inserted, we revalidate the page and redirect the user to a thank-you page.
Rendering the Form on the Page
Now, let’s render the contact form on a page. This component wraps our form and is the main entry point.
This component sets up a simple grid layout and loads the ContactForm component.
Conclusion
Using Next.js, React, and Supabase, we’ve built a fully functional contact form that handles user input, processes the data, and provides real-time feedback to the user during submission. The form is flexible and can be easily adapted to other data sources or platforms.