Back

A Detailed Guide to Server Actions in Next.JS

A Detailed Guide to Server Actions in Next.JS

Next.js provides various techniques for data fetching and mutation on the server, and the choice of technique depends on the specific use case and project requirements. “Server Actions”, a technique for data fetching and mutation on the server, was introduced in Next.js 13.4 as an alpha feature and became stable in Next.js 14, and provides a powerful mechanism for executing code on the server in response to client interactions, offering several advantages over traditional methods, as this article will explain.

Server Actions are asynchronous JavaScript functions that run on the server in response to client interactions. They allow developers to execute server-side code without explicitly defining API routes. They can be invoked from button clicks or form submissions. When a user acts, such as submitting a form or clicking a button, the server is notified, and a specific function is triggered to handle the request. They are typically used to perform tasks that require access to server-side resources, such as databases or file systems.

Benefits of Using Server Actions

There are several benefits of using Server Actions in Next JS, including:

  • Reduced client-side JavaScript: Since Server Actions are defined and run only on the server, they are not included in the client bundle, minimizing the amount of JavaScript code required in the browser, thereby reducing the initial page load time and improving the overall performance.
  • Server-side data mutations: Server Actions enable direct data mutations on the server, eliminating the need to create and manage separate API endpoints. Server Actions simplifies data management and improves performance by avoiding unnecessary network roundtrips.
  • Enhanced accessibility: Since the Server Actions JS code is not shipped to the client, the forms and interactive elements function even with JavaScript disabled to ensure a wider reach for users with accessibility needs.
  • Improved performance: Server Actions enhance page load times and overall application responsiveness by reducing client-side JavaScript and enabling server-side data mutations.
  • Greater Flexibility: Server Actions can handle a wide range of tasks, from simple data retrieval to intricate business logic. This versatility makes them a powerful tool for web development.
  • Support for Progressively Enhanced Forms: Server Actions can be employed to develop progressively enhanced forms. This implies that the forms will remain functional even when JavaScript is disabled while offering an enhanced user experience when JavaScript is enabled.
  • Ability to Revalidate Cached Data: Server Actions can be used to revalidate cached data, ensuring that the data remains up-to-date.
  • Ability to Redirect Users: Server Actions can redirect users after performing a specific action. For instance, they can redirect users to their homepage upon successful login.

How to Create Server Actions

Server Actions can be created in two places.

  • Inside the server component where it is being used.
export default function Product() {
  async function addItemToCart() {
    "use server";
    // Server Action for adding an item to the server-side cart goes here
  }
  return <div>Products Page</div>
}

Notice the use server directive. It instructs Next JS to consider that async function as a Server Action.

  • In a separate file, e.g., actions.js with the use server directive at the top.
"use server";

export async function addItemToCart() {
  // Server Action for adding an item to the server-side cart goes here
}

export async function removeItemFromCart() {
  // Server Action for removing an item from the server-side cart goes here
}

In this method, we can create multiple reusable Server Actions within a single file, making them usable for client and server components.

How to Use Server Actions

Server Actions we created in the above section can be used in various ways.

  • Using the action attribute on the form tag
export default function Product() {
  async function addItemToList() {
    "use server";
    // Server Action goes here
  }

  return (
    <form action={addItemToList}>
      <input type="text" name="item" />
      <button type="submit">Add Item to TODO List</button>
    </form>
  );
}
  • Using the formAction attribute on the children inside the form like button or input.
export default function Product() {
  async function addItemToList() {
    "use server";
    // Server Action goes here
  }

  return (
    <form>
      <input type="text" name="item" />
      <button type="submit" formAction={addItemToList}>Add Item to TODO List</button>
    </form>
  );
}
  • Using startTransition: By wrapping the Server Action in the startTransition function of the useTransition hook in a client component. For instance, In a client component named Form.tsx.
"use client";
import { addItemToList } from "@/lib/actions";
import { useState, useTransition } from "react";

export default function Form() {
  const [item, setItem] = useState("");
  const [isPending, startTransition] = useTransition();

  return (
    <div className="flex flex-col gap-2">
      <input
        type="text"
        value={item}
        onChange={(e) => setItem(e.target.value)}
      />
      <button
        onClick={() => startTransition(() => addItemToList(item))}>
        Add Item to TODO List
      </button>
    </div>
  );
}

We pass the Server Action as a callback to the startTransition function, and the action will be invoked when the button is clicked.

How does it differ from the API route?

There are a few differences between Server Actions and the conventional API routes. Let’s look at that by considering the below scenarios.

Scenario 1

Let’s consider the classic example of an e-commerce application with cart functionality. In the API routes approach, the user makes an HTTP request to the API route with the necessary payload. Subsequently, the API route is executed, the item is added to the cart, and the newly added item is returned to the client. In the client component, we add the newly added item and display the updated cart to the user.

However, in the Server Actions approach, the user can trigger the Server Action to add the item to the cart from a Server Component. The Server Action is executed, and the item is added to the cart on the server. Instead of returning the response to the client and having the client display the updated cart item, the Server Action can revalidate the path and show the updated cart in the Server Component.

Scenario 2

Let’s consider the example of a Blog Application with the usual CRUD operations for Creating, Reading, Updating, and Deleting a blog post, along with other required functionalities. If we use API routes in this case, we can easily expose the API routes to third-party applications that can build their integrations with our blog using the API. However, since Server Actions are tightly coupled with the application, we can’t expose them to any third-party application. In this scenario, Server Actions could be a drawback.

Now comes the obvious question: When or where should I use Server Actions? Well, as you can see from the above, it always comes down to the specific use case. Apart from the benefits that we have seen in the previous sections, you can:

  • Use Server Actions if your Application handles a large number of forms because the Action function automatically receives the FormData when submitted and reduces the pain of maintaining a large number of react states to store the form inputs. Also, they integrate well with React’s experimental useFormStatus and useFormState hooks.
  • Use Server Actions if your Application requires caching and revalidation features because Server Actions, apart from mutating the data, can also be used for cache invalidation and path revalidation.

Example of using Server Action

Let’s see the working of Server Actions by creating and using them in a contacts book application.

Download the starter code by running the below command,

git clone --branch starter git@github.com:nirmal1064/nextjs-server-actions-tutorial.git

## Run the below command to install dependencies and generate Prisma client
npm install
npx prisma db push

Overview of the Starter Code

  • We have bootstrapped a basic Next JS Application with Tailwind CSS, Prisma ORM, utilizing a local SQLite DB. (You can use any database of your choice).
  • We have created two Server components inside the components directory, ContactForm and ContactCard.
  • ContactForm - This component contains a simple form with three input fields for the Name, Phone, and City of a Contact and a button for form submission.
  • ContactCard - This component displays the details of a contact and features two buttons at the bottom for viewing and deleting the contact.
  • The ContactForm and ContactCard are used in the root router’s (/) page.tsx, where the ContactForm is positioned at the top for adding new contacts. In contrast, the list of existing contacts is rendered below using the ContactCard component.
  • Similarly, in the [id]/view route, we display the single contact details using the ContactCard component.
  • In the lib directory, we have the db.ts file where we have configured the database connection.
  • In the schema.prisma file, we have created a Contact model that contains details like name, phone and city of a contact.

Server Action to Add a New Contact

Let’s explore different ways to use Server Actions and demonstrate their functionality.

First, we’ll create a Server Action to add a new contact to the database. This Server Action will be defined within the same file.

Open the ContactForm.tsx file and add the Server Action to add the new contact.

import prisma from "@/lib/db";

export default function ContactForm() {
  async function addContact(formData: FormData) {
    "use server";
    await prisma.contact.create({
      data: {
        name: formData.get("name") as string,
        phone: formData.get("phone") as string,
        city: formData.get("city") as string,
      },
    });
  }

  return (
    <form className="flex flex-col gap-2" action={addContact}>
      <input
        type="text"
        name="name"
        placeholder="Contact Name"
        className="p-2 border border-black"
      />
      <input
        type="text"
        name="phone"
        placeholder="Contact Phone Number"
        className="p-2 border border-black"
      />
      <input
        type="text"
        name="city"
        placeholder="Enter City"
        className="p-2 border border-black"
      />
      <button type="submit" className="bg-blue-500 text-white rounded-md p-1">
        Add Contact
      </button>
    </form>
  );
}

In the ContactForm component, we have created the addContact Server Action, and in the form element, we have passed the addContact to the action attribute. Since we are invoking the Server Action from the form element, the Server Action function implicitly gets an argument of type FormData. We have added the use server directive at the top of the function body. Then, we create a new contact in the database using the Prisma Client’s create API. It extracts data from the formdata argument and creates a new contact object with the specified name, phone number, and city.

That’s it. The Server Action is ready. When a user submits the form, the Server Action will create the new contact and save it to the database. However, the newly added contact won’t be immediately visible in the UI. To address this, we must add the revalidatePath method from the next/cache module.

import { revalidatePath } from "next/cache";

// In the addContact Function
async function addContact(formData: FormData) {
  "use server";
  await prisma.contact.create({
    data: {
      name: formData.get("name") as string,
      phone: formData.get("phone") as string,
      city: formData.get("city") as string,
    },
  });
  // Add the below line
  revalidatePath("/");
}

This will ensure the / path is revalidated once the contact is saved in the database. This ensures that the UI reflects the most recent contact data.

Open the terminal, run the npm run dev command, and go to localhost:3000. In the form, enter the contact details and click on add contact. You should be able to create a new contact, which is instantly updated in the UI. Kindly check the demo below.

Add Contact Demo

The above demo illustrates that a new contact is created when the form is submitted, and the revalidateTag ensures the path is revalidated to show the newly added contact on the screen.

The Server Action code for this section can be found in this GitHub commit.

Note: If we were to implement the same functionality using the API route approach, we would typically create a form component along with React states to handle the form inputs. Subsequently, we must define an API route inside the api directory. Afterward, we would have to make a POST request to the API endpoint to save the contact in the database and return the newly created contact. However, with Server Actions, it is accomplished in a much simpler way.

Server Action to Update an Existing Contact

In the ContactCard component, we have a view button, which will route the user to the [id]/view page, where the details of a specific contact are displayed. We’ll reuse the ContactForm component on this page to provide the update functionality for updating the existing contact. We pass the contact as an optional prop to this component to make the ContactForm reusable for this use case. We are making it optional as we don’t require this prop in case of a new contact.

Modify the ContactForm component to add the optional prop.

import prisma from "@/lib/db";
import { revalidatePath } from "next/cache";

type Props = { contact?: Contact | null };

export default function ContactForm({ contact }: Props) {
  async function addContact(formData: FormData) {
    "use server";
    await prisma.contact.create({
      data: {
        name: formData.get("name") as string,
        phone: formData.get("phone") as string,
        city: formData.get("city") as string,
      },
    });
    revalidatePath("/");
  }

  return (
    <form
      className="flex flex-col gap-2"
      action={contact ? updateContact : addContact}
    >
      <input
        type="text"
        name="name"
        placeholder="Contact Name"
        defaultValue={contact?.name} // Passing the default value in case of update contact
      />
      <input
        type="text"
        name="phone"
        placeholder="Contact Phone Number"
        defaultValue={contact?.phone} // Passing the default value in case of update contact
      />
      <input
        type="text"
        name="city"
        placeholder="Enter City"
        defaultValue={contact?.city} // Passing the default value in case of update contact
      />
      <button type="submit" className="bg-blue-500 text-white rounded-md p-1">
        {/* Changing the button title based on add/update contact */}
        {contact ? "Update Contact" : "Add Contact"}{" "}
      </button>
    </form>
  );
}

Here, we have passed the contact as an optional prop. For an update scenario, the defaultValue attribute of the input elements is utilized to prepopulate the fields with the existing contact information. The button title is dynamically generated based on whether it’s an add or update operation. Similarly, in the action attribute of the form element, we are conditionally calling the addContact or updateContact based on the add/update functionality. The updateContact Server Action will be created in the following step.

Let’s create a Server Action to update an existing contact. In the ContactForm component add the updateContact Server Action.

async function updateContact(formData: FormData) {
  "use server";
  await prisma.contact.update({
    where: { id: contact?.id },
    data: {
      name: formData.get("name") as string,
      phone: formData.get("phone") as string,
      city: formData.get("city") as string
    }
  });
  revalidatePath(`/${contact?.id}/view`);
}

Similar to the ‘addContactServer Action, theupdateContactServer Action also takes theformDataas the input argument and extracts the data from the form fields. Then, for updating the contact, we use Prisma Client’s update API by passing the contact’s ID in the where clause and the form data in the data object. In therevalidatePath` we are revalidating the current contact path to make the UI update instantly.

Now add the ContactForm to the page.tsx inside the app/[id]/view directory as shown below.

import ContactForm from "@/components/ContactForm";
import prisma from "@/lib/db";

type Props = { params: { id: string } };

async function getContactById(id: string) {
  return await prisma.contact.findUnique({ where: { id } });
}

export default async function EditContact({ params }: Props) {
  const contact = await getContactById(params.id);

  return (
    <main className="flex flex-col min-h-screen justify-center items-center bg-gray-50">
      {/* Adding the ContactForm above the Details */}
      <ContactForm contact={contact} />
      <h1 className="font-semibold">Single Contact</h1>
      {contact ? (
        <ContactCard key={contact.id} contact={contact} />
      ) : (
        <p>Contact Does Not Exist</p>
      )}
    </main>
  );
}

Open the single contact by clicking the view button on the contact card. Try editing one of the fields and click on the Update Contact button. This will update the contact in the database and show the updated contact in the UI, as shown in the demo below.

Update Contact Demo

The above demo illustrates that when the form is submitted with different values, the contact is updated, and the revalidateTag ensures the current path is revalidated to show the updated contact instantly.

The Server Action code for this section can be found in this GitHub commit.

Server Action to Delete a Contact

Let’s create a Server Action to implement the delete contact functionality. Delete functionality can be triggered from the homepage displaying all contacts and the single contact view page. However, server components cannot utilize the button’s onClick event handler. We will extract the Delete button into a separate client component to address this. Also, the Server Action must be created in a separate file because we can’t define a Server Action inside a client component. Let’s begin by creating the Server Action.

Create an actions.ts file inside the lib directory and add the Server Action to delete a contact.

"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import prisma from "./db";

export async function deleteContactById(id: string, inViewRoute = false) {
  await prisma.contact.delete({ where: { id } });
  if (inViewRoute) {
    redirect("/");
  } else {
    revalidatePath("/");
  }
}

We have created the Server Action function deleteContactById that deletes a contact from the database. It takes two parameters:

  • Id: The unique identifier of the contact to be deleted. This parameter is passed to the where clause in the Prisma Client’s delete API to delete the specified contact from the DB.
  • inViewRoute: A flag indicating whether the deletion request originates from the homepage or the view page. Based on this flag’s value, the function redirects the user to the homepage or triggers data revalidation if the request comes from the homepage.

Now, let’s create the DeleteButton component inside the components directory and invoke the Server Action from the onClick of the button.

"use client";
import { deleteContactById } from "@/lib/actions";
import { usePathname } from "next/navigation";

type Props = { id: string };

export default function DeleteButton({ id }: Props) {
  const pathName = usePathname();

  return (
    <button
      className="bg-red-600 px-1 py-0.5 rounded-md text-white"
      onClick={() => deleteContactById(id, pathName === "/" ? false : true)}>
      Delete
    </button>
  );
}

We have created the DeleteButton component with contact id as the prop, and we use the usePathname hook to get the current route. The Server Action is triggered through the button’s onClick event handler. As discussed earlier, we can also use the startTransition function of the useTransition hook to invoke the Server Action for non-blocking UI updates.

Now, add the DeleteButton component to the ContactCard component.

import DeleteButton from "./DeleteButton";
import Link from "next/link";

type Props = { contact: Contact };

export default function ContactCard({ contact }: Props) {
  return (
    <div className="w-[250px] bg-white shadow-md p-2">
      <p>
        Name: <span className="font-semibold">{contact.name}</span>
      </p>
      <p>
        Phone: <span className="font-semibold">{contact.phone}</span>
      </p>
      <p>
        City: <span className="font-semibold">{contact.city}</span>
      </p>
      <div className="flex gap-2">
        <Link href={`/${contact.id}/view`}>
          <button className="bg-sky-600 px-1 py-0.5 rounded-md text-white">
            View
          </button>
        </Link>
        {/* Add the Delete Button here */}
        <DeleteButton id={contact.id} />
      </div>
    </div>
  );
}

Open the app in the browser and test the delete functionality from the homepage.

  • Delete contact From the Homepage demo

Delete contact From the Homepage demo

The above demo illustrates that once the delete button is clicked, the contact is deleted, and the revalidateTag ensures the current path is revalidated. The UI is updated to show the available contacts.

  • Delete Contact From the View Page demo

Delete Contact From the View Page demo

The above demo illustrates that as soon as the delete button is clicked, the contact is deleted, and the redirect functionality redirects the user to the homepage, and the available contacts are shown in the UI.

That’s it. The Server Action code for this section can be found in this GitHub commit

Summary

  • Server Actions must be asynchronous functions with the use server directive.
  • They are defined in Server components or a separate file with the use server directive at the top.
  • They can be invoked directly from the action attribute of a form element or the formAction attribute of the buttons inside of a form.
  • Multiple Server Actions accomplishing different tasks can be invoked inside of a single form.
  • They can be invoked from client components, provided that the required Server Action is defined in a separate file.
  • Server Actions can be used to revalidate paths and tags, as well as to redirect users to a different route.

Conclusion

In this article, we have discussed the concept of Server Actions, their benefits over traditional API routes, and different ways of creating and using Server Actions in Next.JS applications. By reducing client-side JavaScript, enhancing accessibility, and enabling server-side data mutations, Server Actions streamline the development process and improve user experience. If you’re looking to build modern web applications with Next.js, Server Actions are an essential tool. Experiment with Server Actions to enhance your applications.

Thanks for Reading. If you find the article useful, do let me know in the comments section. Feel free to comment if you have any questions.

The complete source code of this article can be found on my GitHub. Give it a star if you find the code useful.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay