Integrating Gravity Forms with Next.js

Fri Jan 17 2025

Headless WordPress

Next.js

Integrating Gravity Forms 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, Gravity Forms is a popular, premium solution known for its flexibility. This tutorial shows how to integrate Gravity Forms with Next.js to build a headless contact form using the WordPress REST API.

Prefer to use Contact Form 7? Check out Integrating Contact Form 7 with Next.js.

Gravity Forms

Download Plugin

To get started, ensure you have a Gravity Forms account and license. Download the plugin by navigating to the Downloads section:

Download Gravity Forms plugin

Install Plugin

  1. Open your WordPress Admin Dashboard and go to the Plugins tab in the sidebar.
  2. Click Add New Plugin, then Upload Plugin. Select the zip file you downloaded in the previous section.
  3. Once uploaded, click Install Now and then Activate.

Install Gravity Forms plugin

Activate License

  1. Navigate to the Forms tab in the WordPress sidebar.
  2. Enter your license key in the provided field.
  3. Click Activate License and complete the initial setup.

Activate Gravity Forms license

Create a Form

  1. Click Add New, and select Blank Form.
  2. Name your form.
  3. Add the desired fields, such as “First Name,” “Last Name,” “Email,” and “Message.”

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

Enable REST API

  1. Navigate to Forms → Settings in the WordPress sidebar.
  2. Go to the REST API tab.
  3. Enable access to the API. This step allows external applications to interact with your forms.

Enable API

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

Retrieve Form Field IDs

Before creating your form in Next.js, you need to retrieve the field IDs from Gravity Forms to ensure the field names in your code match those in your Gravity Forms form:

  1. Navigate to the Forms tab in your WordPress dashboard.
  2. Click on the form you created.
  3. Select a field to view its ID in the right-hand sidebar.

Retrieve field ID

Build a Contact Form

Create a Contact Form using shadcn components. When defining the form inputs, ensure the name attributes match the field IDs retrieved from Gravity Forms. Use the format input_ followed by the corresponding field ID:

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="input_1">First name</Label>
<Input id="input_1" name="input_1" />
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="input_3">Last name</Label>
<Input id="input_3" name="input_3" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="input_4">Email</Label>
<Input id="input_4" name="input_4" />
</div>
<div className="space-y-2">
<Label htmlFor="input_5">Message</Label>
<Textarea id="input_5" name="input_5" />
</div>
<Button type="submit">Send</Button>
</div>
</form>
)
}

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

Before sending your form data to the WordPress REST API via a Server Action, retrieve the ID of your Gravity Forms form:

Retrieve Gravity Forms form ID

This ID is required for constructing the WordPress REST API endpoint:

http://your-site.com/wp-json/gf/v2/forms/<FORM_ID>/submissions

Now, create a file for the Server Action to handle form submissions:

src/server/actions.ts
"use server"
export async function sendFormData(formData: FormData) {
try {
const response = await fetch(
// Replace with your site URL and Contact Form ID
"http://wp-gravity-forms-nextjs.local/wp-json/gf/v2/forms/1/submissions",
{
method: "POST",
body: formData,
},
)
const data = await response.json()
console.log(data)
return data
} catch (error) {
console.error("Error submitting form:", error)
}
}

Integrate the sendFormData Server Action into your 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 your form by submitting it. Successful submissions should appear in your Gravity Forms entries:

Gravity Forms entry

Handling Validation

Handle Error Responses

Sending incorrect data, such as leaving a required field blank or entering an invalid email address, gives us the following fields (among others) in the response:

{
"is_valid": false,
"validation_messages": {
"3": "This field is required.",
"4": "The email address entered is invalid, please check the formatting (e.g. email@domain.com)."
}
}

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. Handle errors unrelated to WordPress: We return a generic error saying “An unexpected error has occurred. Please try again later.” in the catch block.

Here’s the updated Server Action:

src/server/actions.ts
"use server"
export async function sendFormData(prevState: unknown, formData: FormData) {
try {
const response = await fetch(
"http://wp-gravity-forms-nextjs.local/wp-json/gf/v2/forms/1/submissions",
{
method: "POST",
body: formData,
},
)
if (response.status === 404) {
throw new Error("No route was found matching the URL and request method.")
}
const data = await response.json()
console.log(data)
return data
} catch (error) {
console.error(error)
return {
error: "An unexpected error has occurred. Please try again later.",
payload: formData,
}
}
}

Now, we can 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 error and confirmation messages 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="input_1">First Name</Label>
<Input id="input_1" name="input_1" />
{state?.validation_messages?.["1"] && (
<p className="text-destructive">
{state.validation_messages["1"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="input_3">Last Name</Label>
<Input id="input_3" name="input_3" />
{state?.validation_messages?.["3"] && (
<p className="text-destructive">
{state.validation_messages["3"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="input_4">Email</Label>
<Input id="input_4" name="input_4" />
{state?.validation_messages?.["4"] && (
<p className="text-destructive">{state.validation_messages["4"]}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="input_5">Message</Label>
<Textarea id="input_5" name="input_5" />
{state?.validation_messages?.["5"] && (
<p className="text-destructive">{state.validation_messages["5"]}</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
{state.is_valid && <p>{state.form.confirmation.message}</p>}
{state.error && <p className="text-destructive">{state.error}</p>}
</div>
</form>
)
}

With this changes in place, the form now displays validation messages returned by Gravity Forms:

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) {
try {
const response = await fetch(
"http://wp-gravity-forms-nextjs.local/wp-json/gf/v2/forms/1/submissions",
{
method: "POST",
body: formData,
},
)
if (response.status === 404) {
throw new Error("No route was found matching the URL and request method.")
}
const data = await response.json()
if (!data.is_valid) {
data.payload = formData
}
console.log(data)
return data
} catch (error) {
console.error(error)
return {
error: "Se ha producido un error. Por favor reinténtelo más tarde",
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="input_1">First Name</Label>
<Input
id="input_1"
name="input_1"
defaultValue={state.payload?.get("input_1") || ""}
/>
{state?.validation_messages?.["1"] && (
<p className="text-destructive">
{state.validation_messages["1"]}
</p>
)}
</div>
<div className="flex-1 space-y-2">
<Label htmlFor="input_3">Last Name</Label>
<Input
id="input_3"
name="input_3"
defaultValue={state.payload?.get("input_3") || ""}
/>
{state?.validation_messages?.["3"] && (
<p className="text-destructive">
{state.validation_messages["3"]}
</p>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="input_4">Email</Label>
<Input
id="input_4"
name="input_4"
defaultValue={state.payload?.get("input_4") || ""}
/>
{state?.validation_messages?.["4"] && (
<p className="text-destructive">{state.validation_messages["4"]}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="input_5">Message</Label>
<Textarea
id="input_5"
name="input_5"
defaultValue={state.payload?.get("input_5") || ""}
/>
{state?.validation_messages?.["5"] && (
<p className="text-destructive">{state.validation_messages["5"]}</p>
)}
</div>
<Button type="submit" disabled={isPending}>
{isPending ? "Sending..." : "Send"}
</Button>
{state.is_valid && <p>{state.form.confirmation.message}</p>}
{state.error && <p className="text-destructive">{state.error}</p>}
</div>
</form>
)
}

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

Form with user inputs

Conclusion

This tutorial covered integrating Gravity Forms 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