Fundamentos do Next.js 15
Server Actions: Mutação de Dados no Servidor
Aprenda sobre server actions: mutação de dados no servidor
Módulo 3: Gerenciamento de Dados e Interatividade
Aula: Server Actions: Mutação de Dados no Servidor 🚀
Bem-vindos à aula sobre Server Actions! Esta é uma funcionalidade poderosa no Next.js que simplifica a mutação de dados e a execução de lógica no servidor, diretamente a partir de interações do cliente.
1. Introdução: O Desafio da Mutação de Dados no Servidor
Tradicionalmente, em aplicações web, quando você precisa realizar uma ação que modifica dados persistentes (como adicionar um item a um banco de dados, atualizar um perfil de usuário, etc.), o fluxo geralmente é o seguinte:
- O usuário interage com um formulário ou botão no cliente.
- O cliente envia uma requisição HTTP (POST, PUT, DELETE) para um endpoint de API (REST ou GraphQL).
- O servidor processa a requisição, interage com o banco de dados e retorna uma resposta.
- O cliente recebe a resposta e atualiza sua UI conforme necessário.
Este modelo funciona, mas pode envolver a criação de endpoints de API separados, gerenciamento de estados de carregamento e erro no cliente, e a necessidade de fetch ou bibliotecas como axios para cada interação.
Server Actions chegam para simplificar esse processo no Next.js App Router. Elas permitem que você defina funções que são executadas diretamente no servidor, mas que podem ser invocadas a partir de eventos do cliente, como a submissão de um formulário.
Em resumo, Server Actions permitem:
- Chamar funções de servidor diretamente de componentes React.
- Mutar dados no servidor de forma segura e eficiente.
- Reduzir a necessidade de endpoints de API REST/GraphQL explícitos para mutações simples.
- Manter a lógica sensível no servidor, longe do cliente.
2. Explicação Detalhada: Como Funcionam as Server Actions
Server Actions são funções assíncronas que você marca explicitamente para serem executadas no servidor. Elas podem ser definidas em qualquer lugar, mas são mais comumente usadas em componentes de servidor, componentes de cliente ou em arquivos separados de utilitários de servidor.
2.1. A Diretiva "use server"
A chave para transformar uma função comum em uma Server Action é a diretiva "use server".
-
No topo de um arquivo: Se você colocar
"use server"no topo de um arquivo, todas as funções exportadas nesse arquivo serão Server Actions. Isso é ideal para arquivos de utilitários que contêm apenas lógica de servidor.// app/lib/actions.ts 'use server'; // Todas as funções exportadas aqui são Server Actions import { revalidatePath } from 'next/cache'; import { db } from './db'; // Supondo um módulo de banco de dados export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; if (!title || !content) { return { error: 'Título e conteúdo são obrigatórios.' }; } await db.post.create({ data: { title, content } }); revalidatePath('/blog'); // Revalida o cache da página do blog return { success: true }; } export async function deletePost(id: string) { await db.post.delete({ where: { id } }); revalidatePath('/blog'); } -
Dentro de uma função: Você pode colocar
"use server"dentro do corpo de uma função específica. Isso permite que você defina Server Actions dentro de arquivos que também contêm lógica de cliente ou outras funções não-server.// app/components/PostForm.tsx // Este é um componente de cliente, mas a Server Action está aninhada import { revalidatePath } from 'next/cache'; import { db } from '@/app/lib/db'; // Supondo um módulo de banco de dados export default function PostForm() { async function handleCreatePost(formData: FormData) { 'use server'; // Apenas esta função é uma Server Action const title = formData.get('title') as string; const content = formData.get('content') as string; if (!title || !content) { // Lógica de erro ou retorno return; } await db.post.create({ data: { title, content } }); revalidatePath('/blog'); } return ( <form action={handleCreatePost}> <input type="text" name="title" placeholder="Título" /> <textarea name="content" placeholder="Conteúdo"></textarea> <button type="submit">Criar Post</button> </form> ); }
2.2. Integração com Formulários HTML
A maneira mais comum de invocar uma Server Action é através da prop action de um elemento <form>.
<form action={yourServerActionFunction}>
{/* Campos do formulário */}
<button type="submit">Enviar</button>
</form>Quando o formulário é submetido, o Next.js intercepta a submissão e chama a yourServerActionFunction no servidor. Os dados do formulário são automaticamente empacotados em um objeto FormData e passados como o primeiro argumento para a Server Action.
2.3. Acesso aos Dados do Formulário (FormData)
Server Actions chamadas por formulários recebem um objeto FormData como seu primeiro argumento. Você pode acessar os valores dos campos do formulário usando formData.get('nomeDoCampo').
async function myServerAction(formData: FormData) {
'use server';
const username = formData.get('username'); // Valor do input com name="username"
const password = formData.get('password'); // Valor do input com name="password"
// ... lógica para processar username e password
}2.4. Retorno de Dados e Tratamento de Erros
Server Actions podem retornar qualquer valor serializável que será recebido no cliente. Isso é útil para feedback de sucesso/erro ou para retornar dados atualizados.
// app/lib/actions.ts
'use server';
export async function updateUserEmail(userId: string, newEmail: string) {
try {
// Lógica para atualizar o email no banco de dados
// ...
return { success: true, message: 'Email atualizado com sucesso!' };
} catch (error) {
console.error('Erro ao atualizar email:', error);
return { success: false, message: 'Falha ao atualizar email.' };
}
}
// No componente cliente (ex: usando useFormState)
import { useFormState } from 'react-dom';
import { updateUserEmail } from '@/app/lib/actions';
function ProfileSettings({ userId, currentEmail }) {
const [state, formAction] = useFormState(
(prevState, formData) => updateUserEmail(userId, formData.get('email') as string),
{ success: false, message: '' } // Estado inicial
);
return (
<form action={formAction}>
<input type="email" name="email" defaultValue={currentEmail} />
<button type="submit">Atualizar Email</button>
{state.message && <p>{state.message}</p>}
</form>
);
}2.5. Revalidação de Cache (revalidatePath, revalidateTag)
Após uma mutação de dados no servidor, a UI do cliente pode precisar ser atualizada para refletir as mudanças. Server Actions se integram perfeitamente com as funções de revalidação de cache do Next.js:
revalidatePath(path: string): Revalida o cache para um caminho específico. Útil quando você sabe que uma mutação afeta os dados exibidos em uma determinada URL.revalidateTag(tag: string): Revalida o cache para uma tag de dados específica. Útil quando você busca dados comfetche os associa a uma tag, e a mutação afeta esses dados.
// app/lib/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from './db';
export async function addTodo(todoText: string) {
await db.todo.create({ data: { text: todoText } });
revalidatePath('/todos'); // Garante que a página /todos seja re-renderizada com o novo todo
}2.6. Segurança
Server Actions são executadas no servidor, o que significa que a lógica sensível (como acesso a banco de dados, chaves de API) permanece segura e não é exposta ao cliente. Além disso, o Next.js automaticamente adiciona proteção contra ataques CSRF (Cross-Site Request Forgery) para Server Actions usadas com formulários.
2.7. useFormStatus e useFormState (Hooks de Cliente)
Embora esta seja uma aula teórica sobre a mutação em si, é importante mencionar dois hooks do React (disponíveis no Next.js) que melhoram a experiência do usuário com Server Actions:
useFormStatus: Permite que componentes clientes leiam o estado de submissão de um formulário (se está pendente, se o formulário foi enviado, etc.) para exibir feedback de UI (ex: desabilitar um botão).useFormState: Permite que você gerencie o estado de um formulário com base no resultado de uma Server Action, retornando um novo estado para o cliente.
3. Código de Exemplo Oficial (Adaptado)
Vamos ver um exemplo prático de como criar um item "todo" usando uma Server Action.
// app/todos/page.tsx
// Este é um Server Component que exibe a lista de todos e o formulário.
import { revalidatePath } from 'next/cache';
import { db } from '@/app/lib/db'; // Supondo que você tenha um módulo de banco de dados simples
// Ações do servidor para manipular os todos
// Definimos as ações em um arquivo separado para melhor organização
// app/lib/actions.ts
// 'use server'; // Já está no topo do arquivo actions.ts
// import { revalidatePath } from 'next/cache';
// import { db } from './db';
export async function addTodo(formData: FormData) {
'use server'; // Esta função é uma Server Action
const todoText = formData.get('todo') as string;
if (!todoText || todoText.trim() === '') {
// Poderíamos retornar um objeto de erro aqui, ou lançar um erro
console.error('O texto do todo não pode ser vazio.');
return;
}
await db.todo.create({
data: { text: todoText.trim(), completed: false },
});
// Revalida o cache da página '/todos' para mostrar o novo item
revalidatePath('/todos');
}
export async function deleteTodo(id: string) {
'use server'; // Esta função é uma Server Action
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}
// ====================================================================
// Componente principal da página /todos
export default async function TodosPage() {
const todos = await db.todo.findMany(); // Busca todos os todos do banco de dados
return (
<div className="max-w-md mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Minha Lista de Tarefas 📝</h1>
<form action={addTodo} className="flex gap-2 mb-6">
<input
type="text"
name="todo"
placeholder="Adicionar nova tarefa..."
className="flex-grow p-2 border border-gray-300 rounded-md"
required
/>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md"
>
Adicionar
</button>
</form>
<ul className="space-y-3">
{todos.map((todo) => (
<li
key={todo.id}
className="flex items-center justify-between bg-gray-100 p-3 rounded-md shadow-sm"
>
<span className={todo.completed ? 'line-through text-gray-500' : ''}>
{todo.text}
</span>
<form action={deleteTodo}>
<input type="hidden" name="id" value={todo.id} />
<button
type="submit"
className="bg-red-500 hover:bg-red-600 text-white text-sm py-1 px-3 rounded-md"
>
Remover 🗑️
</button>
</form>
</li>
))}
</ul>
</div>
);
}
// ====================================================================
// Exemplo de como 'db' e 'todo' poderiam ser definidos (simulação)
// app/lib/db.ts
// import { PrismaClient } from '@prisma/client'; // Exemplo com Prisma
// export const db = new PrismaClient();
// Ou uma simulação simples para fins de exemplo:
// type Todo = { id: string; text: string; completed: boolean };
// let todos: Todo[] = []; // Array em memória para simular um DB
// export const db = {
// todo: {
// findMany: async () => {
// // Simula um delay de DB
// await new Promise(resolve => setTimeout(resolve, 100));
// return todos;
// },
// create: async (data: { data: { text: string; completed: boolean } }) => {
// await new Promise(resolve => setTimeout(resolve, 100));
// const newTodo = { id: String(todos.length + 1), ...data.data };
// todos.push(newTodo);
// return newTodo;
// },
// delete: async (where: { id: string }) => {
// await new Promise(resolve => setTimeout(resolve, 100));
// todos = todos.filter(todo => todo.id !== where.id);
// return { id: where.id }; // Retorna o ID do item deletado
// }
// }
// };Neste exemplo:
- As funções
addTodoedeleteTodosão Server Actions, marcadas com"use server". - Elas recebem o
FormDatado formulário ou um ID diretamente. - Elas interagem com um
dbsimulado (ou real, como Prisma). - Após a mutação,
revalidatePath('/todos')é chamado para garantir que a página/todosseja re-renderizada com os dados mais recentes. - Os formulários
action={addTodo}eaction={deleteTodo}invocam essas Server Actions diretamente.
4. Resumo e Próximos Passos
Resumo da Aula:
- Server Actions são funções executadas no servidor, mas invocadas a partir do cliente, simplificando a mutação de dados.
- A diretiva
"use server"marca uma função ou um arquivo inteiro como Server Action. - Elas se integram nativamente com elementos
<form>através da propaction. - Recebem
FormDatacom os dados do formulário e podem retornar valores serializáveis. - São essenciais para revalidar o cache do Next.js (com
revalidatePatherevalidateTag) após a mutação. - Oferecem segurança intrínseca, mantendo a lógica sensível no servidor e prevenindo CSRF.
Server Actions são uma ferramenta poderosa que reduz a complexidade de gerenciar APIs e mutações, permitindo que você escreva lógica de servidor diretamente onde ela é usada na UI.
Próximos Passos:
- Explore os hooks
useFormStatuseuseFormStatepara adicionar feedback visual e gerenciar o estado da UI durante e após as Server Actions. - Aprofunde-se em como Server Actions podem ser usadas para autenticação e autorização, interagindo com serviços de backend.
- Considere a integração de Server Actions com bibliotecas de validação de esquema (como Zod) para garantir a integridade dos dados antes da mutação.
Parabéns por concluir esta aula sobre Server Actions! Você deu um grande passo para dominar a mutação de dados no Next.js 15. 🎉