Integrating Contact Form 7 with Next.js

Mon Jan 06 2025

Headless WordPress

Next.js

Integrating Contact Form 7 with Next.js Cover

Integrating WordPress with Next.js combines WordPress’s content management power with the flexibility of a modern JavaScript framework. For forms, Contact Form 7 is a popular, free solution known for its simplicity. This tutorial shows how to integrate Contact Form 7 with Next.js to build a headless contact form using the WordPress REST API.

Prefer to use Gravity Forms? Check out Integrating Gravity Forms with Next.js.

Contact Form 7

Install Contact Form 7

To begin, open your WordPress Admin Dashboard, navigate to the Plugins tab in the sidebar, and click Add New Plugin. Search for the Contact Form 7 plugin, install and activate it.

By default, Contact Form 7 doesn’t store submitted messages. If you need to save form submissions in your database, you can install Flamingo, a free WordPress plugin created by the same author as Contact Form 7.

Create a Form

Navigate to the Contact tab in WordPress sidebar, click Add New, and name your form. Then, define the fields you want to include, such as “First Name”, “Last Name”, “Email” and “Message”:

New form with first name, last name, email and message as fields

Configure the Mail tab

Adjust the Mail tab settings to match your field names and ensure emails are sent correctly:

Mail tab settings

By default, Flamingo retrieves values from Contact Form 7’s default input fields: your-subject, your-name, and your-email. If your contact form doesn’t include these default fields, the Subject and From fields will not display correctly.

To resolve this, you can customize the Flamingo settings in the Additional Settings tab:

flamingo_name: "[first-name] [last-name]"
flamingo_email: "[email]"
flamingo_subject: "Form submission"

Next.js

Install Next.js

To create a project, ensure you have Node.js installed and run the following command:

Terminal window
npx create-next-app@latest

Give your project a name and select the default options for all prompts. Then, wait for the required dependencies to install.

Install shadcn (Optional)

This tutorial uses shadcn to build the form with pre-designed components. To install, run:

Terminal window
npx shadcn@latest init

Next, install the components needed for our form: button, input, textarea and label:

Terminal window
npx shadcn@latest add button input textarea label

Build a Contact Form

Create a ContactForm using shadcn components:

src/components/ContactForm.tsx
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Textarea } from "./ui/textarea"
export function ContactForm() {
return (
<form action="" className="w-full max-w-3xl rounded-lg bg-white px-4 py-12">
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" name="first-name" />
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" name="last-name" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" />
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" name="message" />
</div>
<Button type="submit">Send</Button>
</div>
</form>
)
}

Ensure that the field names in this form (e.g. first-name) match those in your Contact Form 7 form. This is crucial for mapping the data correctly when sending it to the WordPress REST API.

Next, remove the default Next.js boilerplate code from your homepage and replace it with the ContactForm component:

src/app/page.tsx
import { ContactForm } from "@/components/ContactForm"
export default function HomePage() {
return (
<main className="flex min-h-screen items-center justify-center bg-slate-200">
<ContactForm />
</main>
)
}

Start your Next.js app by running:

Terminal window
npm run dev

Go to http://localhost:3000 in your browser to see your form:

Next.js form

Implement Server Action

To send our form to the WordPress REST API, we will use a Server Action. Before proceeding, retrieve the following details from the Contact Form 7 - Edit Form screen:

  1. Contact Form ID: This is an integer found in the URL as post=<FORM_ID>
  2. Unit Tag: This is a 7-character alphanumeric string located in the form’s shortcode

Retrieve Contact Form 7 id and unit tag

  • The contact form ID will be used directly in our WordPress REST API endpoint:

    http://your-site.com/wp-json/contact-form-7/v1/contact-forms/<FORM_ID>/feedback
  • The unit tag will be appended to the body of the form as a value with a key of _wpcf7_unit_tag

Now we can implement our Server Action. Create a server directory inside src and add an actions.ts file:

src/server/actions.ts
"use server"
export async function sendFormData(formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39") // Replace with your Unit Tag
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://yt-cf7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
console.log(data)
} catch (error) {
console.error("Error submitting form:", error)
}
}

Use the server action you created to handle form submissions in the ContactForm component:

src/components/ContactForm.tsx
import { sendFormData } from "@/server/actions"
export function ContactForm() {
return (
<form
action=""
action={sendFormData}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
{/* Form fields */}
</form>
)
}

After completing the setup, test the form by submitting it. You should receive a response like this:

{
"contact_form_id": 8,
"status": "mail_sent",
"message": "Thank you for your message. It has been sent.",
"posted_data_hash": "3669e828ea771249bc2935dd42dd9773",
"into": "#21e9d39",
"invalid_fields": []
}

You should also receive a notification email and see the form submission in Flamingo - Inbound Messages:

Flamingo inbound messages

Handling Validation

Handle Error Responses

Sending incorrect data, such as leaving a required field blank or entering an invalid email address, gives us a response like this:

{
"contact_form_id": 8,
"status": "validation_failed",
"message": "One or more fields have an error. Please check and try again.",
"invalid_fields": [
{
"field": "last-name",
"message": "Please fill out this field.",
"idref": null,
"error_id": "21e9d39-ve-last-name"
},
{
"field": "email",
"message": "Please enter an email address.",
"idref": null,
"error_id": "21e9d39-ve-email"
}
],
"posted_data_hash": "",
"into": "#21e9d39"
}

Currently, the user isn’t be notified if something goes wrong. We need to display these errors on the front end to address this.

useActionState Hook

To handle validation messages, we use the useActionState React hook. This hook helps manage state transitions for server actions, such as displaying validation errors or showing a success message.

To use it, transform the ContactForm server component into a client component by adding "use client" at the top of the file.

src/components/ContactForm.tsx
"use client"
import { useActionState } from "react"
export function ContactForm() {
const [state, action, isPending] = useActionState(sendFormData, {})
return (
<form
action={sendFormData}
action={action}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
{/* Form fields */}
</form>
)
}

We also need to make changes to the server action. Specifically:

  1. Add an Extra Parameter: We include the prevState parameter because it is required by the useActionState hook, but we type it as unknown since it is not utilized.

  2. Restructure Validation Messages: Instead of returning an array of invalid field messages, we transform it into an object where:

    • The key is the field name
    • The value is the corresponding error message

    This makes it easier to access validation errors on the front end.

  3. Handle General Messages: The response includes a general message, which can be displayed at the bottom of the form. For errors unrelated to WordPress, a generic message—“An unexpected error has occurred. Please try again later.”—is returned.

Here’s the updated server action:

src/server/actions.ts
"use server"
export async function sendFormData(prevState: unknown, formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39")
try {
const response = await fetch(
"http://wp-contact-form-7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
if (data.invalid_fields.length) {
data.invalid_fields_obj = {}
data.invalid_fields.forEach(
(field: { field: string; message: string }) => {
data.invalid_fields_obj[field.field] = field.message
},
)
}
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
}
}
}

We also utilize the isPending variable from our hook to disable the submit button while the submission is being processed:

src/components/ContactForm.tsx
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>

Display Validation Errors

With the updated server action, we can now conditionally render error messages below their respective fields and display the general message at the bottom of the form:

src/components/ContactForm.tsx
export function ContactForm() {
const [state, action, isPending] = useActionState(sendFormData, {})
return (
<form
action={action}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" name="first-name" />
{state?.invalid_fields_obj?.["first-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["first-name"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" name="last-name" />
{state?.invalid_fields_obj?.["last-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["last-name"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" />
{state?.invalid_fields_obj?.["email"] && (
<p className="text-destructive">
{state.invalid_fields_obj["email"]}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" name="message" />
{state?.invalid_fields_obj?.["message"] && (
<p className="text-destructive">
{state.invalid_fields_obj["message"]}
</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
<p className={state?.status === "mail_sent" ? "" : "text-destructive"}>
{state?.message}
</p>
</div>
</form>
)
}

With this changes in place, the form now displays validation messages returned by Contact Form 7:

Form with validation errors

Preserve User Inputs

When an error occurs, however, all the form fields are cleared, forcing the user to refill the form entirely. This creates a poor user experience. To address this issue, we can send the form data back from the server action to the front end whenever there is an error:

src/server/actions.ts
"use server"
export async function sendFormData(prevState: unknown, formData: FormData) {
formData.append("_wpcf7_unit_tag", "21e9d39")
try {
const response = await fetch(
"http://wp-contact-form-7-nextjs.local/wp-json/contact-form-7/v1/contact-forms/8/feedback",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
if (data.invalid_fields.length) {
data.payload = formData
data.invalid_fields_obj = {}
data.invalid_fields.forEach(
(field: { field: string; message: string }) => {
data.invalid_fields_obj[field.field] = field.message
},
)
}
return data
} catch (error) {
console.error("Error submitting form:", error)
return {
message: "An unexpected error has occurred. Please try again later.",
payload: formData,
}
}
}

With the original form data available on the front end when an error occurs, we can set it as the default value for each field. If the form is submitted successfully, the payload won’t be sent back to the front end, and the fields will be cleared:

src/components/ContactForm.tsx
export function ContactForm() {
const [state, action, isPending] = useActionState(sendFormData, {})
return (
<form
action={action}
className="w-full max-w-3xl rounded-lg bg-white px-4 py-12"
>
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-center text-4xl">Contact</h1>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input
id="first-name"
name="first-name"
defaultValue={state.payload?.get("first-name") || ""}
/>
{state?.invalid_fields_obj?.["first-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["first-name"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input
id="last-name"
name="last-name"
defaultValue={state.payload?.get("last-name") || ""}
/>
{state?.invalid_fields_obj?.["last-name"] && (
<p className="text-destructive">
{state.invalid_fields_obj["last-name"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
defaultValue={state.payload?.get("email") || ""}
/>
{state?.invalid_fields_obj?.["email"] && (
<p className="text-destructive">
{state.invalid_fields_obj["email"]}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea
id="message"
name="message"
defaultValue={state.payload?.get("message") || ""}
/>
{state?.invalid_fields_obj?.["message"] && (
<p className="text-destructive">
{state.invalid_fields_obj["message"]}
</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
<p className={state?.status === "mail_sent" ? "" : "text-destructive"}>
{state?.message}
</p>
</div>
</form>
)
}

Now, the form preserves the user’s inputs whenever an error occurs:

Form with user inputs

Conclusion

This tutorial covered how to integrate Contact Form 7 with Next.js to create a headless form submission system. By following these steps, you can leverage WordPress’s content management capabilities while maintaining the flexibility of Next.js.

With this approach, you can:

  • Streamline form submissions using the WordPress REST API
  • Build dynamic forms with Next.js
  • Effectively handle validation and user inputs