Server Actions

Server action is an asynchronous function designed to be executed on the server. They can be called directly inside a component, whether it is client or server component, which makes them the perfect choice for handling form submissions without having to create a separate API endpoint.

Define a server action

Server actions can be defined by creating a actions.js under the src directory.

text
1src
2├── actions.js       <===== Server actions
3└── app
4    ├── favicon.ico
5    ├── globals.css
6    ├── layout.jsx
7    └── page.jsx     <===== Webpage

app/actions.js

jsx
1"use server";
2
3export async function createUser(data) {
4  const name = data.get("name");
5  const email = data.get("email");
6
7  // Simulate saving to a database
8  console.log(name);
9  console.log(email);
10}

There are a few things you must pay attention here, first of all, the actions.js file must start with a "use server" directive. This tells Next.js that all the functions defined in this file must be executed on the server, which is the whole purpose of a server action.

Next, as an example, let's create a createUser() function. When the form is submitted, this function will be executed. The form data will be passed along to createUser(), allowing the function to process them in the backend.

And because the network calls involved are always asynchronous, all the server action functions must be marked as async as well.

By default, the function createUser() accepts a FormData object input argument. The form data will be passed automatically when the form is submitted, as we are going to see later.

Inside the function, each individual form input can be access using the get() method.

javascript
1const name = data.get("name");

Again, we are simulating the process of saving the values to the database by logging them to the console instead. We'll discuss how to work with databases in future lessons.

Import server action into component

The server action can then be imported inside any pages or components:

app/users/page.jsx

jsx
1import { createUser } from "@/actions";
2
3export default async function Page() {
4  return (
5    <form action={createUser} method="post">
6      <label htmlFor="name">Name:</label>
7      <input
8        type="text"
9        id="name"
10        name="name"
11        placeholder="Enter your name"
12        required
13      />
14      <label htmlFor="email">Email:</label>
15      <input
16        type="email"
17        id="email"
18        name="email"
19        placeholder="Enter your email"
20        required
21      />
22      <button type="submit">Submit</button>
23    </form>
24  );
25}

This page contains a form with two inputs, name and email.

In line 5, for the action attribute, instead of specifying an API endpoint, we pass the createUser server action instead.

Notice that we did not define the input argument for createUser. When the form is submitted, Next will automatically wrap up the form inputs and pass it to the server action as FormData.

User form

And in the backend, the form inputs will be accessed and processed.

app/actions.js

jsx
1"use server";
2
3export async function createUser(data) {
4  const name = data.get("name"); // Retrieves the value of the input field with `name="name"`
5  const email = data.get("email"); // Retrieves the value of the input field with `name="email"`
6
7  // Simulate saving to a database
8  console.log(name);
9  console.log(email);
10}

Note that the name attribute of each <input> field corresponds to the argument used in the get() method to retrieve its value.

Name attribute matches get argument

Fill in the information and submit the form, you should see the following logs in the console:

User action log

Passing additional arguments to server actions

Sometimes, you may want to send some extra information to the backend, aside from the user input.

For example, you have an online shopping site, and you want to track what product has been purchased by each customer. Or, if you need to know the location of the user, in order to provide customized services.

Of course, you can design the form to ask the user to provide their id, the product id, or their location, but that would be a significant negative impact on the user experience.

In practice, it makes for sense for you to retrieve these information programmatically, and send them to the backend along with user input.

There are two ways you can do this with server actions. First, you can embed the information with hidden input fields. For example:

page.jsx

jsx
1import { createUser } from "@/actions";
2
3export default async function Home() {
4  // Retrieve some data programmatically
5  const secret = "123";
6
7  return (
8    <form action={createUser} method="post">
9      <div>
10        <label htmlFor="name">Name:</label>
11        <input
12          type="text"
13          id="name"
14          name="name"
15          placeholder="Enter your name"
16          required
17        />
18      </div>
19      <div>
20        <label htmlFor="email">Email:</label>
21        <input
22          type="email"
23          id="email"
24          name="email"
25          placeholder="Enter your email"
26          required
27        />
28      </div>
29      {/* Embed the secret here */}
30      <input type="hidden" id="secret" name="secret" value={secret} required />
31      <button type="submit">Submit</button>
32    </form>
33  );
34}

In line 5, we are hardcoding the secret, but in practice, the information should be retrieved programmatically, such as from the user session, the browser API, remote API endpoint, or some other places.

And line 30, the retrieved data will be embedded inside a type="hidden" input field.

jsx
1<input type="hidden" id="secret" name="secret" value={secret} required />

When the form is submitted, the value of this hidden form will also get transferred to the server action as part of the FormData, which can then be accessed like this:

actions.js

javascript
1"use server";
2
3export async function createUser(data) {
4  const name = data.get("name");
5  const email = data.get("email");
6  const secret = data.get("secret"); // Access the data like
7
8  // Simulate saving to a database
9  console.log(name);
10  console.log(email);
11  console.log(secret);
12}

Notice that the hidden form will not be displayed when the webpage is rendered:

User form

But when you submit the form, the secret will be transferred to the server action, and gets logged like this:

Logged secret

However, there are security risks with this setup. Even though, as you can see in the above image, the hidden field is not visible, it can still be accessed when you view the source code of the rendered webpage.

Hidden input field accessible from source code

If the information you are working with is sensitive, this may lead to security risks.

Instead, you can pass the additional information using the bind() method.

jsx
1import { createUser } from "@/actions";
2
3export default async function Home() {
4  const secret = "123";
5  const createUserWithSecret = createUser.bind(null, secret);
6
7  return (
8    <form action={createUserWithSecret} method="post">
9      . . .
10    </form>
11  );
12}

We discussed bind() when we cover how to resolve the losing this issue for JavaScript functions.

javascript
1var obj = {
2  name: "John",
3  sayName: function () {
4    console.log(this.name);
5  },
6};
7
8var say = obj.sayName; // store the function as a variable
9say(); // logs "undefined" (global scope)
10
11var boundSay = obj.sayName.bind(obj); // bind to the original object
12boundSay(); // logs "John"

It is used to set the context for a function, and in the above example, the context for sayName is set to obj. Without it, the context will be lost when sayName is saved as a variable, or passed as a callback.

In our example, the context is set to null, as it is a standalone function, not part of an object.

Sounds unrelated to our problem here, but in fact, bind() also accepts extra argument aside from the context, and these arguments will be prepend to the original function arguments.

function bind

In our createUser server action function, this extra argument can be accessed like this:

javascript
1"use server";
2
3export async function createUser(secret, data) {
4  const name = data.get("name");
5  const email = data.get("email");
6
7  // Simulate saving to a database
8  console.log(name);
9  console.log(email);
10  console.log(secret);
11}