Next.js, the popular React framework, has introduced a powerful feature called Server Actions. This feature allows developers to run asynchronous code directly on the server from their client-side components, bridging the gap between client and server-side operations. In this article, we’ll explore Server Actions in depth, looking at both data mutations and data fetching scenarios.
What are Server Actions?
Server Actions are functions that run on the server but can be called from the client. They provide a seamless way to perform server-side operations like database queries or API calls without the need to create separate API routes. This approach simplifies the development process and can lead to more efficient applications.
Server Actions for Data Mutations
Let’s start by looking at how Server Actions can be used for data mutations, such as adding a new item to a database.
Example: Adding a Todo Item
// app/actions.js
'use server'
import { revalidatePath } from 'next/cache'
export async function addTodo(formData) {
const todo = formData.get('todo')
// Simulate adding to a database
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('Added todo:', todo)
revalidatePath('/')
}
// app/page.js
import { addTodo } from './actions'
export default function Home() {
return (
<form action={addTodo}>
<input type="text" name="todo" />
<button type="submit">Add Todo</button>
</form>
)
}
In this example:
- We define a Server Action
addTodo
inactions.js
. The'use server'
directive at the top of the file indicates that all exports from this file are Server Actions. - The
addTodo
function receivesformData
as an argument, which contains the form data submitted by the client. - We simulate adding the todo to a database with a timeout. In a real application, you’d interact with your database here.
- We use
revalidatePath('/')
to update the page after the todo is added. - In
page.js
, we import theaddTodo
action and use it directly in theaction
prop of the form.
When the form is submitted, the addTodo
action is called on the server, processing the form data. This approach allows you to write server-side logic right alongside your client-side components, making it easier to manage data mutations.
Server Actions for Data Fetching
While Server Actions are primarily designed for data mutations, they can also be used for data fetching. However, there are some considerations to keep in mind when using them for GET requests.
Example: Fetching Todo Items
// app/actions.js
'use server'
export async function getTodos() {
// Simulate fetching from a database
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{ id: 1, text: 'Learn Next.js' },
{ id: 2, text: 'Build a project' },
{ id: 3, text: 'Deploy to production' }
];
}
// app/page.js
import { getTodos } from './actions'
export default async function Home() {
const todos = await getTodos();
return (
<div>
<h1>My Todos</h1>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
)
}
In this example:
- We define a Server Action
getTodos
that simulates fetching data from a database. - In
page.js
, we import thegetTodos
action and use it directly in the component. - Since we’re using the action in a Server Component, we can await it directly in the component.
- The fetched todos are then rendered in the component.
Using Server Actions in Client Components
For Client Components, you’ll need to handle the asynchronous nature of Server Actions differently. Here’s an example:
'use client'
import { useState } from 'react'
import { useTransition } from 'react'
import { getTodos } from './actions'
export default function TodoList() {
const [todos, setTodos] = useState([])
const [isPending, startTransition] = useTransition()
const handleFetchTodos = () => {
startTransition(async () => {
const fetchedTodos = await getTodos()
setTodos(fetchedTodos)
})
}
return (
<div>
<button onClick={handleFetchTodos} disabled={isPending}>
{isPending ? 'Loading...' : 'Fetch Todos'}
</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
)
}
In this Client Component example, we use the useTransition
hook to handle the asynchronous nature of the Server Action, providing a loading state while the data is being fetched.
Considerations for Using Server Actions
While Server Actions are powerful, there are some important points to consider:
- Server Actions are primarily designed for data mutations (POST, PUT, DELETE) rather than data fetching.
- For simple GET requests, you might want to use Next.js’s built-in data fetching methods instead.
- For more complex data fetching scenarios, especially those involving caching and revalidation, consider using Next.js’s data fetching methods like
fetch
with thecache
option, or libraries like React Query or SWR. - Server Actions are great for combining data fetching with data mutations in a single action, which can be more efficient than separate API calls.
💁 Check out our other articles😃
👉 Generate a free Developer Portfolio website with AI prompts
👉 Build a Simple Calculator App with React, Tailwind CSS. Displays History
Conclusion
Server Actions in Next.js provide a powerful way to bridge the gap between client and server-side operations. They simplify the process of performing server-side tasks from client-side components, leading to more efficient and easier-to-manage code. While they excel at data mutations, they can also be used for data fetching with some considerations. As you build your Next.js applications, consider how Server Actions can enhance your development process and improve your application’s performance.