<ggalupo />

Handle form submission with server actions in Next.js

Learn how to use server actions in forms and improve your Next.js application performance today

6m read

The newly created server components architecture, introduced in React 18, has added new ways to handle everyday situations.

However, keeping up with the rapid pace of innovations, especially in the JavaScript ecosystem, is not always an easy task, especially when old solutions still work well.

With that in mind, this article aims to present a new possibility for form submission: server actions.

Server actions are asynchronous functions only executed on the server, primarily used to handle data mutation operations (addition, editing, or deletion).

Throughout the article, we will use adding a new task to a task list as an example to make it easier to understand.

Required version

Server actions were introduced in Next.js 13, but only became stable and safe for use in a production environment on version 14, which was released in October 2023.

In React, they will be available for use on version 19, which has been in beta since April 2024 and is expected to be released soon.

Creating a server action

A server action can be defined by using the "use server" directive, in two different ways:

On the first line of an asynchronous function body, within a server component, to mark only that function as a server action.

AddTask.tsx
1export const AddTask = () => {2  const addTask = async () => {3    "use server";4    // Server-side code5  };67  // ...8};

If you choose to use the approach above, the component must be a server component.

On the first line of a file that exports multiple asynchronous functions, to mark all of them as server actions.

actions.ts
1"use server";23export const addTask = async () => {4  // Server-side code5};67export const deleteTask = async () => {8  // Server-side code9};

Handling form submission without using server actions (traditional way)

When submitting a form, it's common to create a function that will be passed to the onSubmit event, handling the entire process on client side.

With this approach, it becomes necessary to create a separate API route, which will be called by this function and take care of adding the task on the server, since it's essential to be in a secure environment to access the database and persist the new record.

route.ts
1export async function POST() {2  // Server-side code34  return new Response("Created!", {5    status: 201,6  });7}
AddTask.tsx
1"use client";23import { type FormEvent, useState } from "react";45export const AddTask = () => {6  const [task, setTask] = useState("");7  const [isPending, setIsPending] = useState(false);89  const handleSubmit = async (e: FormEvent) => {10    e.preventDefault();11    setIsPending(true);1213    try {14      await fetch("/api/task", {15        method: "POST",16        body: JSON.stringify({ task }),17      });18      // Handle success19    } catch (e) {20      // Handle error21    } finally {22      setIsPending(false);23    }24  };2526  return (27    <form onSubmit={handleSubmit}>28      <label htmlFor="task-name">New task</label>29      <input30        id="task-name"31        name="task"32        value={task}33        onChange={(e) => setTask(e.target.value)}34      />35      <button type="submit">{isPending ? "Adding..." : "Add task"}</button>36    </form>37  );38};

Handling form submission using server actions

With the addition of server actions, React chose to extend the <form> tag. Now, the action prop not only supports the default behavior of receiving an URL, but also allows a server action to be passed, which will be executed upon form submission.

The action will be invoked with a single argument, containing the form data of the submitted form, as can be seen in the server component below.

AddTask.tsx
1export const AddTask = () => {2  const addTask = async (formData: FormData) => {3    "use server";4    const taskName = formData.get("task-name");5     // Server-side code6  };78  return (9    <form action={addTask}>10      <label htmlFor="task-name">New task</label>11      <input id="task-name" name="task" />12      <button type="submit">Add task</button>13    </form>14  );15};

Using server actions from a client component

If the form is a client component, it can still utilize server actions to handle its submission.

For this, it will be necessary to create the server action using the second alternative presented at the beginning of the article, which consists of declaring the function in a separate file and then importing it into the component.

AddTask.tsx
1"use client";23import { addTask } from "@/actions";45export const AddTask = () => {6  return (7    <form action={addTask}>8      <label htmlFor="task-name">New task</label>9      <input id="task-name" name="task" />10      <button type="submit">Add task</button>11    </form>12  );13};

With this implementation, although the JavaScript bundle sent to the browser may be slightly larger, there are also some advantages, such as greater ease in resetting the form to its initial state after a successful submission, or the possibility of using more robust input validators on the client side.

Handling loading state

To handle the submit’s loading state, it's a good practice to abstract the necessary logic into an isolated component, which can be reused in many forms of the application.

To achieve this, a client component should be created. This component will check the current status of the form using the useFormStatus hook, in addition to rendering the button responsible for submission.

Submit.tsx
1"use client";2import { useFormStatus } from "react-dom";34export const Submit = () => {5  const { pending } = useFormStatus();67  return (8    <button type="submit" disabled={pending}>9      {pending ? "Adding..." : "Add"}10    </button>11  );12};

This component can be used in forms that have server actions, giving the user visual feedback while the operation is not yet completed on the server.

AddTask.tsx
1import { Submit } from "@/components/form/submit";23export const AddTask = () => {4  const addTask = async (formData: FormData) => {5    "use server";6    const taskName = formData.get("task-name");7    // Server-side code8  };910  return (11    <form action={addTask}>12      <label htmlFor="task-name">New task</label>13      <input id="task-name" name="task" />14      <Submit />15    </form>16  );17};

Finally, what are the main advantages of using server actions in forms?

Progressive enhancement

Using server actions is a massive benefit for users with slow internet connections or even those who have disabled JavaScript in their browser, since the submission works independently of it.

Smaller JavaScript bundle sent to the client

Due to this independence, the amount of JavaScript code sent to the client side is reduced, improving the performance and loading time of the application.

Reduced complexity

Server actions prevent the need for creating a separate API route to handle server-side actions, in addition to being reusable among the application's forms.

This way, an extra development step is eliminated, reducing the complexity of the application.

Next improvements

The useActionState hook will be released in React 19, further simplifying the way forms are handled through server actions. However, to keep the article from becoming too lengthy, this topic will be explored more thoroughly in a future post.

Visit useActionState docs