Exploring Next.js Server Actions

Exploring Next.js Server Actions

Next.js has always been a strong framework for building full-stack apps, and with the introduction of server actions in version 13.4, it has become even more powerful.
So, what are server actions?
Server actions are asynchronous functions executed on the server, and they can be called from both server and client components. To better understand the concept, let's start with an example.

First example

Here is our first server action: an async function called handleSignIn. This TypeScript file begins with the "use server" directive, indicating that this function will run on the server. In this example, we simply log the received data.

"use server";

export async function handleSignIn(formData: FormData) {
  console.log(formData);
}

On the other hand, we have a server component that renders a sign-in form, icluding fields for email + password, as well ass a submit button.

import { handleSignIn } from "@/shared/test";
import { Input } from "@/shared/ui-kit/shadcn/input";
import { Label } from "@/shared/ui-kit/shadcn/label";

export default function SignInPage() {
  return (
    <form action={handleSignIn}>
      <div>
        <Label htmlFor="email">Email</Label>
        <Input
          type="email"
          id="email"
          name="email"
          required
          placeholder="Enter your email"
        />
      </div>

      <div>
        <Label htmlFor="password">Password</Label>
        <Input
          type="password"
          id="password"
          name="password"
          required
          placeholder="Enter your password"
        />
      </div>

      <button type="submit">Sign In</button>
    </form>
  );
}

The handleSignIn function acts as the action for this form. When the button is clicked, the form sends a POST request with the entered email and password to the server, where they are logged. This entire process works even if JavaScript is disabled on the client. Cool! Now let’s dive into how server actions work under the hood.

Server actions under the hood

If we inspect the network tab during the form submission, we’ll see a POST request with a payload that includes the form data: the action ID, email and password.

What exactly is the action ID?
Let's explore our JavaScript bundle to understand what Next.js is doing behind the scenes. For easier reading in this article, the code in the next section has been simplified and shortened.

  1. Defining a server action: In this step, we set up our server action in a file that uses the "use server" directive.

  2. Registering the server action: In this step, Next.js maps all functions within the file and registers each one with a unique ID using the createServerReference function. This unique ID is crucial for linking the client-side call to the corresponding server action. Using the unique ID, Next.js creates an endpoint for the server action, enabling seamless communication between the client and server.

     function createServerReference(id, callServer, encodeFormAction) {
       var proxy = function () {
         var args = Array.prototype.slice.call(arguments);
    
         return callServer(id, args);
       };
    
       registerServerReference(proxy, {
         id: id,
         bound: null,
       });
    
       return proxy;
     }
    
  3. Triggering the Server Action: When a server action is triggered from the client-side application, Next.js invokes the fetchServerAction function. This function is responsible for sending a POST request to the server, which includes the unique ID of the action as well as any necessary data, or payload, required by the server. The unique ID acts as a key that ensures the correct server action is called. Along with the ID, the payload contains the relevant data (such as form inputs or other parameters) needed for the server to process the request.

async function fetchServerAction(state, nextUrl, param) {
  let { actionId, actionArgs } = param;
  const body = await encodeReply(actionArgs);

  const res = await fetch("", {
    method: "POST",
    headers: {
      Accept: _approuterheaders.RSC_CONTENT_TYPE_HEADER,
      [_approuterheaders.ACTION]: actionId,
      [_approuterheaders.NEXT_ROUTER_STATE_TREE]: encodeURIComponent(
        JSON.stringify(state.tree)
      ),
      ...(nextUrl
        ? {
            [_approuterheaders.NEXT_URL]: nextUrl,
          }
        : {}),
    },
    body,
  });
}

conclusion: Under the hood, Next.js automatically creates an endpoint that allows the client to execute the handleSignIn function on the server. This simplifies the process by eliminating the need for manually defining an API route and sending a fetch request. With Next.js, this entire workflow is streamlined through the use of an asynchronous function combined with the "use server" directive, making server-side actions more seamless and efficient.

Benefits of Using Server Actions:

  • Streamline server-side logic implementation without the need to manually define API routes.

  • Simplify client-side data mutations or queries by directly invoking server actions.

  • Enable efficient cache revalidation using tags or paths with just a single network request. Learn more about cache revalidation in Next.js

  • Perform mutations easily, even without JavaScript, by using the form's action attribute.

The Best Way to Write Server Actions:

After exploring various tools, I discovered a library called ZSA, and it’s a true game changer. It allows you to write type-safe, clean server actions while providing an excellent developer experience. Here’s why ZSA stands out:

  • Validated inputs/outputs with Zod.

  • Procedures (aka middleware) that pass context to your server actions.

  • React Query integration for querying server actions in client components.

  • Server actions can even be exposed as RESTful endpoints within your application.

Ready to dive in? Explore the ZSA documentation to get started