Course
Server Actions I
Next.js Mastery: From Fundamentals to Full-Stack
Unlock the power of Next.js with this comprehensive course! Starting with the basics, you’ll learn essential skills such as routing, data fetching, and styling. Progress through practical projects, including building your own React Notes app, to gain hands-on experience. Dive into the source code to understand the inner workings and core principles of Next.js. Perfect for developers with a basic knowledge of React and Node.js, this course ensures you’ll be ready to create high-performance full-stack applications efficiently. Join us and master Next.js, backed by industry experts and a supportive learning community.
Server Actions
Server Actions refer to asynchronous functions that are executed on the server side. They can be used in both server and client components in Next.js applications to handle data submissions and mutations.
Note: "data query" refers to reading data. Though "data mutation" might sound unfamiliar at first, it’s a key concept in handling data changes in applications.
Basic Usage
To define a Server Action, you need to use the
"use server"
directive in React. There are two ways to apply this directive:- Function-Level Usage: Place
"use server"
at the top of anasync
function to designate it as a Server Action. - Module-Level Usage: Place
"use server"
at the top of a file to make all exported functions from that file Server Actions.
Server Actions can be used in both server and client components:
In Server Components: Both function-level and module-level usages are supported.
// app/page.jsxexport default function Page() { // Server Action async function create() { 'use server' // ... } return ( // ... )}
In Client Components: Only module-level usage is supported. You need to create a separate file, commonly named "actions," and apply the
"use server"
directive at the top.'use server'
// app/actions.jsexport async function create() { // ...}
And then import the file like this:
import { create } from '@/app/actions' export function Button() { return ( // ... )}
You can also pass Server Actions as props to client components:
<ClientComponent updateItem={updateItem} />
'use client'
export default function ClientComponent({ updateItem }) { return <form action={updateItem}>{/* ... */}</form>}
Use Cases
In the Pages Router, interacting with the backend typically requires defining an API endpoint and making calls to it. However, in the App Router, these operations can be simplified using Server Actions. If you are implementing functionality that would traditionally require creating an API for frontend-backend communication, consider using Server Actions—unless you specifically need an external API.
Server Actions are often used with
<form>
elements, but they can also be triggered within event handlers, useEffect
, third-party libraries, or other form elements like <button>
.Practical Example
Let’s create a simple ToDo List to compare traditional API usage in the Pages Router with Server Actions in the App Router.
Pages Router - API
First, we create a
/api/todos
endpoint:// app/api/todos/route.jsimport { NextResponse } from 'next/server'
const data = ['Reading', 'Writing', 'Meditation']
export async function GET() { return NextResponse.json({ data })}
export async function POST(request) { const formData = await request.formData() const todo = formData.get('todo') data.push(todo) return NextResponse.json({ data })}
Visiting
/api/todos
gives you a list of todos.
Next, create a form page:
// pages/form.jsimport { useEffect, useState } from "react"
export default function Page() { const [todos, setTodos] = useState([])
useEffect(() => { const fetchData = async () => { const { data } = await (await fetch('/api/todos')).json() setTodos(data) } fetchData() }, [])
async function onSubmit(event) { event.preventDefault() const response = await fetch('/api/todos', { method: 'POST', body: new FormData(event.currentTarget), }) const {data} = await response.json() setTodos(data) }
return ( <> <form onSubmit={onSubmit}> <input type="text" name="todo" /> <button type="submit">Submit</button> </form> <ul> {todos.map((todo, i) => <li key={i}>{todo}</li>)} </ul> </> )}
This form submits data to
/api/todos
, which updates the list of todos.
App Router - Server Actions
Now, let’s implement the same ToDo List using Server Actions:
// app/form2/page.jsimport { findToDos, createToDo } from './actions';
export default async function Page() { const todos = await findToDos(); return ( <div className="max-w-sm mx-auto mt-8 p-4 bg-white shadow-md rounded-lg"> <h2 className="text-xl font-bold mb-4">Enter Task</h2> <form action={createToDo} className="mb-4"> <input type="text" name="todo" className="w-full p-2 mb-2 border border-gray-300 rounded" placeholder="Enter a new task" /> <button type="submit" className="w-full bg-black text-white py-2 rounded hover:bg-gray-800" > Add </button> </form> <ul className="space-y-2"> {todos.map((todo, i) => ( <li key={i} className="flex justify-between items-center"> <span>{todo}</span> <button className="bg-black text-white px-4 py-1 rounded text-sm hover:bg-gray-800"> Delete </button> </li> ))} </ul> </div> )}
Here’s the
actions.js
file:'use server'
import { revalidatePath } from "next/cache";
const data = ['Reading', 'Writing', 'Meditation']
export async function findToDos() { return data}
export async function createToDo(formData) { const todo = formData.get('todo') data.push(todo) revalidatePath("/form2"); return data}
The effect is as follows:
Server Actions Explained
Basic Principle
Server Actions are implemented by sending a POST request to the current page’s URL when a form is submitted. Next.js automatically inserts a hidden
<input>
with a value like $ACTION_ID_xxxxxxxx
, which helps identify the correct action on the server.
Upon form submission, the server processes the form data, triggers the relevant action, and returns the updated RSC Payload, which is used to update the UI with the latest data.
Sending a POST request:
Payload with
$ACTION_ID
Returns the RSC Payload, which is used to render the updated data
In short:
- Server Actions utilize the POST request method internally, accessing the current page address and differentiating based on
$ACTION_ID
. - Integration of Server Actions with Next.js caching and revalidation architecture enables Next.js to provide updated UI and new data simultaneously when an Action is called.
Benefits of Using Server Actions
- Cleaner Code: No need to manually create API endpoints. Since Server Actions are just functions, they can be reused throughout your application.
- Progressive Enhancement: Even with JavaScript disabled, forms using Server Actions will still work by reloading the page upon submission.
Key Considerations
- Serialization: Server Actions' arguments and return values must be serializable (i.e.,
JSON.stringify
should work without errors). - Inheritance: Server Actions inherit runtime and configuration options from the page or layout where they are used, including fields like
maxDuration
.
Advanced Usage | Triggering Actions in Event Handlers
You can trigger Server Actions from event handlers. For example, let’s add a "Add Exercise" button to our ToDo List:
// app/form2/page.jsimport { findToDos, createToDo } from './actions';import Button from './button';
export default async function Page() { const todos = await findToDos(); return ( <div className="max-w-sm mx-auto mt-8 p-4 bg-white shadow-md rounded-lg"> <h2 className="text-xl font-bold mb-4">Enter Task</h2> <form action={createToDo} className="mb-4"> <div className="flex space-x-2"> <input type="text" name="todo" className="flex-grow p-2 border border-gray-300 rounded" placeholder="Enter a new task" /> <Button type="submit" className="px-4"> Add </Button> </div> </form> <Button className="w-full mb-2 bg-black text-white py-2 rounded hover:bg-gray-800"> Add Exercise </Button> <ul className="space-y-2"> {todos.map((todo, i) => ( <li key={i} className="flex justify-between items-center"> <span>{todo}</span> <button className="bg-black text-white px-4 py-1 rounded text-sm hover:bg-gray-800"> Delete </button> </li> ))} </ul> </div> )}
The button component:
// app/form2/button.js'use client'import { createToDoDirectly } from './actions';export default function Button({ children, className }) {return ( <button onClick={async () => { const data = await createToDoDirectly('Exercise') alert(JSON.stringify(data)) }} className={`bg-black text-white py-2 rounded hover:bg-gray-800 ${className}`} > {children} </button>);}
Add the new action to
actions.js
:export async function createToDoDirectly(value) { const form = new FormData() form.append("todo", value); return createToDo(form)}
The effect is as follows:
Conclusion
By now, you should have a solid understanding of how to use Server Actions in Next.js. Server Actions have become a key feature in Next.js since version 14, and mastering them is essential for building full-stack applications in Next.js.
In future lessons, we’ll dive deeper into Server Actions, covering topics like handling form submission states, server-side validation, optimistic updates, error handling, accessing cookies and headers, and implementing redirects.