DEV Community

Jonathan U
Jonathan U

Posted on

Next.js: Three Ways to Call Server Actions from Client Components

In Next.js, Server Actions are asynchronous functions that execute on the server. They can be used in Client Components to handle form submissions and perform data mutations. More on Server Actions can be read here.

There are a few ways to use these Server Actions in the Client Component.

  • Form Action
  • Event Handlers
  • useEffect

Form Action

React allows Server Actions to be invoked with the action prop. When a user submits a form, the action is invoked, which will receive the FormData object.

Let's say you need a way to track how many bananas each monkey in the zoo has. There's a PostgreSQL table called monkeys. The form may look like this:

<form action={addMonkey}>
  <input type="text" id="name" name="name" required/>
  <input type="number" id="numBananas" name="numBananas" defaultValue={0} required/>
  <input type="submit" defaultValue="Submit"/>
</form>
Enter fullscreen mode Exit fullscreen mode

Notice how the action will invoke a Server Action called addMonkey. This method is defined with the React "use server" directive.

"use server";

export async function addMonkey(formData: FormData): Promise<boolean> {
  const schema = z.object({
    name: z.string().min(1),
    numBananas: z.number().int()
  });
  const monkeyParse = schema.safeParse({
    name: formData.get("name"),
    numBananas: z.coerce.number().parse(formData.get("numBananas"))
  });

  if (!monkeyParse.success) {
    return false;
  }

  const data = monkeyParse.data;

  try {
    await sql`
        INSERT INTO monkeys (name, numBananas)
        VALUES (${data.name}, ${data.numBananas})
    `;

    revalidatePath("examples/server-actions")
    return true;
  } catch (e) {
    return false;
  }

}
Enter fullscreen mode Exit fullscreen mode

Event Handlers

You can call Server Actions from Event Handlers, such as onClick. For example, to increment the number of bananas a monkey has:

"use client";

import {Monkey, updateNumBananas} from "@/app/examples/server-actions/server_actions";

export default function MonkeyList(props: { monkeys: Monkey[] }) {
  return (
    <ul>
      {props.monkeys.map((monkey) => {
        return <li key={monkey.id}>
          <span>{monkey.name} has {monkey.numbananas} banana(s).</span>
          <button onClick={async () => {
            await updateNumBananas(monkey.id, monkey.numbananas + 1)
          }}>
            Increment Bananas
          </button>
        </li>
      })}
    </ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

This adds a button next to each monkey to increment the bananas count by 1. The Server Action looks like this:

"use server";

export async function updateNumBananas(id: number, numBananas: number) {
  try {
    await sql`
        UPDATE monkeys 
        SET numBananas = ${numBananas}
        WHERE id = ${id}
    `;

    revalidatePath("examples/server-actions")
    return true;
  } catch (e) {
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

useEffect

Lastly, Server Actions can be called in the React useEffect hook when the component mounts or a dependency changes. For example, to display the number of monkeys from a Server Action:

"use client";

import {getMonkeyCount, Monkey, updateNumBananas} from "@/app/examples/server-actions/server_actions";
import {useEffect, useState} from "react";

export default function MonkeyList(props: { monkeys: Monkey[] }) {
  const [monkeyCount, setMonkeyCount] = useState(0);

  useEffect(() => {
    const updateMonkeyCount = async () => {
      const updatedNumMonkeys = await getMonkeyCount();
      setMonkeyCount(updatedNumMonkeys);
    }
    updateMonkeyCount();
  })

  return (
    <>
      <p>Monkeys: {monkeyCount}</p>
      <ul>
        {props.monkeys.map((monkey) => {
          return <li key={monkey.id}>
            <span>{monkey.name} has {monkey.numbananas} banana(s).</span>
            <button onClick={async () => {
              await updateNumBananas(monkey.id, monkey.numbananas + 1)
            }}>
              Increment Bananas
            </button>
          </li>
        })}
      </ul>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this example, useEffect is triggered automatically and will update the number of monkeys when a new one is added.
The server action looks like this:

"use server";

export async function getMonkeyCount() {
  const numMonkeys = await sql`
    SELECT *
    FROM monkeys;
  `;

  return numMonkeys.count;
}
Enter fullscreen mode Exit fullscreen mode

There are better and more optimized ways to show the number of monkeys but this is just an example to show how we can use Server Actions in Client Components.

Source code of the complete example: https://github.com/juhlmann75/Next.js-Examples/tree/main/src/app/examples/server-actions

Top comments (0)