Skip to main content

Command Palette

Search for a command to run...

Nuances of Server Actions in Next.js

Never be confused again.

Updated
7 min read
Nuances of Server Actions in Next.js
T

Just a guy who loves to write code and watch anime.

Introduction

Server Actions in Next.js can be confusing to understand.

I already dove into Server Actions in my previous post on RSCs.

My aim here is to dive into the nuances of Server Actions.

When to use bind and why?

A common pattern when doing form submissions: Use bind with server actions in Next.js is to pass additional arguments to the action beyond just the submitted form data.

If you look at this code example:

// actions.ts
"use server";

export async function deleteAction(id, formData) {
  // Delete the post with the given id
  await deletePost(id);

  // Perform any other necessary operations using formData
  // ...

  // Revalidate or redirect as needed
  revalidatePath("/blog");
}

// BlogPost.tsx
import { deleteAction } from "./actions";

export default function BlogPost({ post }) {
  const deletePost = deleteAction.bind(null, post.id);

  return (
    <form action={deletePost}>
      <button type="submit">Delete</button>
    </form>
  );
}

Here, bind is used to create a new function deletePost that has the id argument pre-bound to it. When the form is submitted, deletePost will be invoked as the server action, and Next.js will send the id along with the form data to the server.

The reason for using bind is that server actions in Next.js only receive the form data as the argument when invoked. If you need to pass any additional data to the action that is not part of the form, you need to use bind to create a new function with the extra arguments pre-bound.

Alternative

The other approach you can take is to use hidden input fields.

export default function BlogPost({ post }) {
  return (
    <form action={deleteAction}>
      <input type="hidden" name="id" value={post.id} />
      <button type="submit">Delete</button>
    </form>
  );

This is quite popular to do when working with Remix.

It's gonna include the hidden inputs values in the form data too.

Works like a Queue

Let's say you've multiple Server Actions on a page. You're implementing something highly dynamic where users can click around to trigger different actions and you expect them to run concurrently.

// FileUpload.jsx
export async function deleteAction(id) {
  "use server";
  // Delete the file with the given id
  // ...
}

export default function FileUpload({ file }) {
  const deleteFile = deleteAction.bind(null, file.id);

  return (
    <div>
      <p>{file.name}</p>
      <Form action={deleteFile}>
        <button type="submit">Delete</button>
      </Form>
    </div>
  );
}

// FileList.jsx
import FileUpload from "./FileUpload";

export default function FileList({ files }) {
  return (
    <div>
      {files.map((file) => (
        <FileUpload key={file.id} file={file} />
      ))}
    </div>
  );
}

Here, we've a list of files and each file has a delete button. When the user clicks the delete button, the file should be deleted.

Now, let's say the user tries to delete 3 files at once. What happens?

You'd expect the files to be deleted concurrently. But that's not what happens. Server Actions run sequentially. They work like a queue.

This is one of the current limitations of Server Actions. Or to be honest, it might be intentional.

Under the hood

Under the hood, when a Server Action is invoked, Next.js sends a POST request to an internally generated API endpoint.

The upsides of having the actions run sequentially:

  • Consistency: Ensure a predictable order of execution.

  • Avoid race conditions: Sequential flow avoids conflicts where two or more actions try to modify the same data.

Inline Server Actions

export default function Page() {
  async function action(formData) {
    "use server";
    // Server action logic
  }

  return <form action={action}>...</form>;
}

I had two confusions with this piece of code:

  1. Why would you want to use them?

  2. Wait, why do we need "use server" when we're in a server component?

Inline Actions

You don't have to define inline actions. They can be useful when the action is specific to a single component and not reused elsewhere in the application.

Personally, I don't think I would ever define an inline action. It just looks weird to me.

"use server" in a server component

"use server" is still necessary to use in a server component when defining an inline action.

Server actions are different from regular server-side code. They are specifically designed to be invoked from the client-side, usually through form submissions or other user interactions.

"use server" exposes server code to the client.

"use server" under the hood

Unique Identifier

Next.js creates a unique identifier for each server action. This identifier links the client-side request to the correct server-side function. This ensures that the server knows which function to execute when the client triggers the action.

API Endpoint

Next.js automatically generates an API endpoint for each server action. These endpoints are created during the compilation process and are not visible in your codebase. The generated endpoints handle the incoming requests from the client and route them to the corresponding server action.

Client-Side Invocation

When you invoke a server action from the client-side, such as through a form submission or a button click, Next.js sends a POST request to the generated API endpoint.

The request includes a special header called "Next-Action" which contains the unique identifier of the server action.

This header is automatically added by Next.js and is used to map the request to the corresponding server action.

Always gotta be async

Under the hood, server actions are by nature asynchronous. This is because they are designed to perform server-side operations that may take some time to complete.

Hence you should always use the async keyword when defining a server action.

Error Handling

Error handling is done by returning an error in the catch block of a try-catch statement.

That's then sent up to your nearest error UI boundary which you define like error.tsx.

export async function deleteAction(id, formData) {
  try {
    // Delete the post with the given id
    await deletePost(id);

    // Perform any other necessary operations using formData
    // ...

    // Revalidate or redirect as needed
    revalidatePath("/blog");
  } catch (error) {
    return { message: "An error occurred while deleting the post." };
  }
}

You can still throw an error. Do it if it's an unexpected error.

Return: Handle expected errors.

Throw: Handle unexpected errors.

Revalidation

Server Actions integrate with Next.js' caching and revalidation architecture. When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip.

There are two main ways to revalidate data after a Server Action:

  1. revalidatePath: Revalidates data for a specific path. It accepts a relative URL string where it will clear the cache and revalidate the data for that path e.g. /blog.

  2. revalidateTag: Revalidates data associated with a specific cache tag. Next.js has a cache tagging system for invalidating fetch requests across routes.

revalidatePath

Example: After creating a new todo item via a Server Action, you can revalidate the / path to ensure the list of todos is updated.

// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function addTodo(data: FormData) {
  // Save new todo to database
  await createTodo(data);

  // Revalidate the "/" path
  revalidatePath("/");
}

If you don't do this, the user will have to refresh the page to see the new todo item.

revalidateTag

The way this works: You can tag fetch requests with one or more tags

const res = await fetch("https://...", {
  next: { tags: ["todos"] },
});

Then call revalidateTag to revalidate all entries with that tag. This works across routes.

// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";

export async function addTodo(data: FormData) {
  // Save new todo to database
  await createTodo(data);

  // Revalidate data tagged with 'todos'
  revalidateTag("todos");
}

fetch and tags

On the server

The fetch call with cache tags is used on the server-side in Next.js, not on the client. It's typically used inside Server Components, Route Handlers, or Server Actions.

In contrast, libraries like React Query are primarily used for client-side data fetching and caching

Works flawlessly

Next.js' built-in data fetching and caching capabilities, including cache tags, are designed to work seamlessly with the framework's server-rendering architecture.

They provide a way to fine-tune caching and revalidation behavior on the server.

Practical scenario

Using multiple tags for a single fetch call can be useful in scenarios where the fetched data is associated with multiple entities or categories.

It allows for more granular control over cache invalidation.

Let's look at an example where a post may be associated with multiple categories:

// Fetching a blog post associated with multiple categories
const res = await fetch(`https://api.example.com/posts/${postId}`, {
  next: { tags: ["posts", `category:${category1}`, `category:${category2}`] },
});

In this case, the blog post is tagged with a general 'posts' tag and specific category tags like 'category:tech' and 'category:javascript'.

This allows for targeted cache invalidation:

  • Invalidating the 'posts' tag will revalidate all blog posts.

  • Invalidating a specific category tag like 'category:tech' will revalidate only posts in that category.

W

Server actions in Next.js bring powerful capabilities, allowing developers to execute server-side logic directly within the framework. These actions handle tasks like data fetching, API interactions, and server-side rendering (SSR) seamlessly. One of the key nuances is understanding when to use server-side rendering versus static generation for optimizing performance. Additionally, handling server actions efficiently can improve load times and scalability. If you’re new to server actions, enrolling in a web developer course that covers frameworks like Next.js can help you better understand how to leverage these features for building fast and dynamic web applications. Visit this link to enhance your skills https://itcourses.au/courses/web-development-training-course/

S

This post effectively emphasizes the YAGNI (You Aren't Gonna Need It) principle in software development, advocating for simplicity in addressing current requirements without over-engineering for potential future needs.

The discussion on the pros and cons of external versus in-house solutions is particularly insightful. While external services can introduce dependencies and learning curves, building your own solution may divert focus from core business objectives. The example of feature flagging illustrates this point well, highlighting how teams often only need basic functionality rather than all the bells and whistles of a comprehensive external tool.

Overall, this perspective encourages a pragmatic approach, prioritizing immediate needs while avoiding unnecessary complexity.

1
A

I found a wonderful site for the treatment of psoriasis and this site is called - https://valhallavitality.com/blog/exploring-the-potential-of-rapamycin-in-the-treatment-of-psoriasis , here I was helped to relieve the symptoms of this disease and greatly reduce the appearance of spots on the skin, so whoever is useful and who has this disease should try it!

1
M

This was a refreshing read coming from a long docs sesh. Goes well with security-nextjs-server-components-actions#data-access-layer blog

1
J

Server actions in Next.js streamline client-server interactions, allowing async form submissions and handling server-side logic like deletions or updates. They run sequentially, providing consistency and preventing race conditions. Using bind helps pass additional data, while revalidation ensures fresh UI updates.

1
L

Stumbled upon spin samurai while looking for a new online casino, and it quickly became a favorite. The site’s design was clean and easy to use, making the gaming experience enjoyable from the get-go. The variety of bonuses, especially the welcome package, was impressive and added to the overall fun. Each visit felt rewarding, with new promotions always available to keep things interesting.

P
PDF Flex1y ago

This Casino has become a real discovery for me - Casino Regina . I like that there is always something new and interesting here - regular game updates, new promotions and bonuses. The interface is very user-friendly, and even if you are a beginner, it will not be difficult to understand everything. Another nice thing is the speed of payments - money is credited to the account very quickly. I also appreciate the support team, which is always ready to help and answers questions very quickly. This casino has become my favorite place to play, and I'm happy to recommend it to others.

M

"Error handling is done by returning an error in the catch block of a try-catch statement.

That's then sent up to your nearest error UI boundary which you define like error.tsx."

I'm pretty sure the error is sent up only if you throw?

3
S

Very informative, I really could not understand why my nextjs app is slow despite using server-actions and caching. Looks like if you are building a portal, it's unlikely to be greatly scalable with server actions.

4