Fundamentos do Next.js 15
Implementando Formulários Completos com Server Actions
Aprenda sobre implementando formulários completos com server actions
🚀 Implementando Formulários Completos com Server Actions no Next.js 15
Bem-vindos ao módulo de Gerenciamento de Dados e Interatividade! Nesta aula prática, vamos mergulhar no mundo dos formulários no Next.js, utilizando uma das features mais poderosas e elegantes para lidar com submissões de dados: as Server Actions. Prepare-se para simplificar seu código e criar experiências de usuário mais fluidas e seguras! ✨
1. Introdução: Formulários Full-Stack com Server Actions
Tradicionalmente, a submissão de formulários em aplicações web envolve uma comunicação cliente-servidor através de APIs REST ou GraphQL. No Next.js, isso geralmente significava criar uma API Route (/api/minha-acao) para lidar com a lógica do servidor e, no lado do cliente, usar fetch para enviar os dados.
Com as Server Actions, o Next.js 15 nos permite definir funções que executam diretamente no servidor, mas que podem ser chamadas a partir de componentes React no cliente. Isso elimina a necessidade de criar endpoints de API separados para ações simples, tornando o desenvolvimento de formulários muito mais coeso e "full-stack".
Nesta aula, você aprenderá a:
- Criar Server Actions para processar dados de formulários.
- Gerenciar estados de carregamento (
pending) comuseFormStatus. - Manipular respostas do servidor e exibir mensagens de erro/sucesso com
useFormState. - Revalidar dados e redirecionar usuários após a submissão.
Vamos construir um formulário completo, robusto e eficiente! 💪
2. Explicação Detalhada com Exemplos
O que são Server Actions?
Server Actions são funções assíncronas que são executadas no servidor. Elas podem ser definidas diretamente dentro de componentes do lado do cliente ou em arquivos separados (que devem ser marcados com "use server").
Quando uma Server Action é invocada a partir do cliente, o Next.js lida automaticamente com a serialização dos dados, a chamada RPC (Remote Procedure Call) para o servidor e a desserialização da resposta.
2.1. Criando sua Primeira Server Action para Formulários
Para usar uma Server Action com um formulário, você a atribui diretamente à prop action do elemento <form>. A Server Action receberá automaticamente um objeto FormData como seu primeiro argumento.
// app/components/FormularioDeTarefa.tsx
'use client'; // Este componente é um Client Component
import { adicionarTarefa } from '@/app/actions/tarefas'; // Importa a Server Action
export function FormularioDeTarefa() {
return (
<form action={adicionarTarefa} className="space-y-4 p-4 border rounded shadow-md">
<div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700">
Título da Tarefa:
</label>
<input
type="text"
id="titulo"
name="titulo" // Importante: 'name' para o FormData
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="descricao" className="block text-sm font-medium text-gray-700">
Descrição:
</label>
<textarea
id="descricao"
name="descricao" // Importante: 'name' para o FormData
rows={3}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
></textarea>
</div>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Adicionar Tarefa
</button>
</form>
);
}Agora, vamos criar a Server Action adicionarTarefa:
// app/actions/tarefas.ts
'use server'; // Marca este arquivo como um Server Module
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Simula um banco de dados simples
let tarefas: { id: string; titulo: string; descricao: string; concluida: boolean }[] = [];
let nextId = 1;
export async function adicionarTarefa(formData: FormData) {
// Simula um atraso de rede
await new Promise(resolve => setTimeout(resolve, 1000));
const titulo = formData.get('titulo') as string;
const descricao = formData.get('descricao') as string;
if (!titulo) {
// Em um cenário real, você retornaria um objeto de erro
throw new Error('O título da tarefa é obrigatório!');
}
const novaTarefa = {
id: String(nextId++),
titulo,
descricao,
concluida: false,
};
tarefas.push(novaTarefa);
console.log('Tarefa adicionada:', novaTarefa);
// Revalida o cache para a página inicial, para que a nova tarefa apareça
revalidatePath('/');
// Opcional: redireciona o usuário para outra página após o sucesso
// redirect('/');
}
// Função para obter tarefas (para exibir na página, por exemplo)
export async function getTarefas() {
'use server'; // Esta também é uma Server Action/Function
return tarefas;
}2.2. Lidando com Estados de Carregamento (pending) com useFormStatus
Quando um formulário é submetido, é uma boa prática desabilitar o botão de submissão e/ou exibir um indicador de carregamento para o usuário. O hook useFormStatus (do React DOM) é perfeito para isso! Ele retorna o status da última submissão de formulário do seu ancestral mais próximo.
// app/components/BotaoDeSubmissao.tsx
'use client';
import { useFormStatus } from 'react-dom'; // Importa do React DOM
export function BotaoDeSubmissao() {
const { pending } = useFormStatus(); // Obtém o status de "pending"
return (
<button
type="submit"
aria-disabled={pending} // Acessibilidade: indica que o botão está desabilitado
disabled={pending} // Desabilita o botão enquanto a ação está pendente
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{pending ? 'Adicionando...' : 'Adicionar Tarefa'}
</button>
);
}Agora, atualize seu FormularioDeTarefa para usar este botão:
// app/components/FormularioDeTarefa.tsx
'use client';
import { adicionarTarefa } from '@/app/actions/tarefas';
import { BotaoDeSubmissao } from './BotaoDeSubmissao'; // Importe o novo componente
export function FormularioDeTarefa() {
return (
<form action={adicionarTarefa} className="space-y-4 p-4 border rounded shadow-md">
{/* ... outros campos ... */}
<div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700">
Título da Tarefa:
</label>
<input
type="text"
id="titulo"
name="titulo"
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="descricao" className="block text-sm font-medium text-gray-700">
Descrição:
</label>
<textarea
id="descricao"
name="descricao"
rows={3}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
></textarea>
</div>
<BotaoDeSubmissao /> {/* Use o componente do botão aqui */}
</form>
);
}2.3. Manipulando Respostas e Erros com useFormState
Para exibir mensagens de sucesso ou erro do servidor de volta no cliente, podemos usar o hook useFormState (também do React DOM). Ele permite que uma Server Action retorne um estado que pode ser lido e atualizado pelo componente do cliente.
useFormState recebe dois argumentos:
- A Server Action que você quer usar.
- Um estado inicial.
Ele retorna um array com o estado atual e uma nova Server Action (que você usará na prop action do formulário).
// app/components/FormularioDeTarefaComEstado.tsx
'use client';
import { useFormState } from 'react-dom';
import { adicionarTarefaComEstado } from '@/app/actions/tarefas';
import { BotaoDeSubmissao } from './BotaoDeSubmissao';
// Definimos um tipo para o estado que a Server Action retornará
interface FormState {
message: string;
success: boolean;
errors?: {
titulo?: string[];
descricao?: string[];
};
}
const initialState: FormState = {
message: '',
success: false,
};
export function FormularioDeTarefaComEstado() {
// useFormState retorna o estado atual e uma nova action (que encapsula a original)
const [state, formAction] = useFormState(adicionarTarefaComEstado, initialState);
return (
<form action={formAction} className="space-y-4 p-4 border rounded shadow-md">
<h2 className="text-xl font-bold mb-4">Adicionar Nova Tarefa</h2>
{state.message && (
<p className={`text-sm ${state.success ? 'text-green-600' : 'text-red-600'}`}>
{state.message}
</p>
)}
<div>
<label htmlFor="titulo" className="block text-sm font-medium text-gray-700">
Título da Tarefa:
</label>
<input
type="text"
id="titulo"
name="titulo"
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
{state.errors?.titulo && (
<p className="mt-1 text-sm text-red-500">{state.errors.titulo.join(', ')}</p>
)}
</div>
<div>
<label htmlFor="descricao" className="block text-sm font-medium text-gray-700">
Descrição:
</label>
<textarea
id="descricao"
name="descricao"
rows={3}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
></textarea>
{state.errors?.descricao && (
<p className="mt-1 text-sm text-red-500">{state.errors.descricao.join(', ')}</p>
)}
</div>
<BotaoDeSubmissao />
</form>
);
}E a Server Action atualizada para retornar o estado:
// app/actions/tarefas.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Simula um banco de dados simples
let tarefas: { id: string; titulo: string; descricao: string; concluida: boolean }[] = [];
let nextId = 1;
// Definimos o tipo para o estado retornado pela Server Action
interface FormState {
message: string;
success: boolean;
errors?: {
titulo?: string[];
descricao?: string[];
};
}
export async function adicionarTarefaComEstado(
prevState: FormState, // useFormState passa o estado anterior como primeiro argumento
formData: FormData
): Promise<FormState> { // A Server Action agora retorna um Promise<FormState>
await new Promise(resolve => setTimeout(resolve, 1000)); // Simula atraso
const titulo = formData.get('titulo') as string;
const descricao = formData.get('descricao') as string;
// Validação simples
if (!titulo || titulo.trim() === '') {
return {
message: 'Falha ao adicionar tarefa.',
success: false,
errors: {
titulo: ['O título da tarefa é obrigatório.'],
},
};
}
if (titulo.length < 3) {
return {
message: 'Falha ao adicionar tarefa.',
success: false,
errors: {
titulo: ['O título deve ter pelo menos 3 caracteres.'],
},
};
}
const novaTarefa = {
id: String(nextId++),
titulo: titulo.trim(),
descricao: descricao ? descricao.trim() : '',
concluida: false,
};
tarefas.push(novaTarefa);
console.log('Tarefa adicionada:', novaTarefa);
revalidatePath('/'); // Revalida o cache da página inicial
return {
message: 'Tarefa adicionada com sucesso!',
success: true,
};
// Se você quiser redirecionar, use redirect() aqui.
// Lembre-se que `redirect` lança um erro, então ele interrompe a execução.
// Se você usar `redirect`, a mensagem de sucesso acima não será exibida.
// redirect('/');
}
export async function getTarefas() {
'use server';
return tarefas;
}2.4. Revalidação de Dados e Redirecionamento
revalidatePath(path): Invalida o cache para um path específico, garantindo que a próxima requisição para aquela rota buscará os dados mais recentes. Essencial para exibir dados atualizados após uma Server Action.revalidateTag(tag): Invalida o cache para dados associados a uma tag específica. Útil se você estiver usandofetchcomnext.tags.redirect(path): Redireciona o usuário para uma nova rota. Importante:redirect()lança um erro, o que interrompe a execução da Server Action. Portanto, ele deve ser a última instrução em um caminho de sucesso.
Ambos revalidatePath e redirect são importados de next/cache e next/navigation respectivamente e devem ser usados dentro de Server Actions.
3. Exercícios Práticos: Construindo um Sistema de Gerenciamento de Tarefas
Vamos aplicar o que aprendemos construindo um pequeno sistema de gerenciamento de tarefas.
🎯 Desafio: Sistema de Gerenciamento de Tarefas
Seu objetivo é criar uma página que exiba uma lista de tarefas e permita adicionar novas tarefas usando um formulário completo com Server Actions.
📝 Task List:
- Configuração Inicial:
- Crie um novo projeto Next.js 15 (se ainda não tiver um):
npx create-next-app@latest my-todo-app --ts --app --tailwind - Limpe o
app/page.tsxpara começar do zero.
- Crie um novo projeto Next.js 15 (se ainda não tiver um):
- Crie o Módulo de Server Actions:
- Crie um arquivo
app/actions/tarefas.ts. - Adicione a Server Action
adicionarTarefaComEstadoe a funçãogetTarefasconforme os exemplos acima. - Certifique-se de incluir a linha
"use server";no topo do arquivo.
- Crie um arquivo
- Crie o Componente
BotaoDeSubmissao:- Crie um arquivo
app/components/BotaoDeSubmissao.tsx. - Implemente o componente que usa
useFormStatuspara desabilitar o botão e mudar o texto durante a submissão. Lembre-se de"use client";.
- Crie um arquivo
- Crie o Componente
FormularioDeTarefaComEstado:- Crie um arquivo
app/components/FormularioDeTarefaComEstado.tsx. - Implemente o formulário completo usando
useFormStatepara exibir mensagens de sucesso/erro e oBotaoDeSubmissao. Loremn-se de"use client";.
- Crie um arquivo
- Exiba a Lista de Tarefas na Página Principal:
- Modifique
app/page.tsxpara ser um Server Component. - Importe e chame
getTarefas()para obter a lista de tarefas. - Renderize a lista de tarefas e o
FormularioDeTarefaComEstado. - Dica: Como
getTarefasé uma Server Action, você pode chamá-la diretamente de um Server Component.
- Modifique
- Teste Completo:
- Inicie o servidor de desenvolvimento:
npm run dev - Acesse a página e teste:
- Submeta uma tarefa sem título (deve exibir erro).
- Submeta uma tarefa com título curto (deve exibir erro).
- Submeta uma tarefa válida (deve exibir mensagem de sucesso e a tarefa deve aparecer na lista automaticamente).
- Observe o botão de submissão desabilitar durante o envio.
- Inicie o servidor de desenvolvimento:
- (Opcional) Adicione uma ação de "Concluir Tarefa":
- Crie uma nova Server Action
concluirTarefa(id: string). - Adicione um botão "Concluir" ao lado de cada tarefa na lista.
- Quando clicado, este botão deve chamar a Server Action
concluirTarefa(você pode usar a propformActionem um<button type="submit">ou um<form action={...}>com um campohiddenpara o ID). concluirTarefadeve atualizar o status da tarefa e revalidar a página.
- Crie uma nova Server Action
// app/page.tsx
import { getTarefas } from '@/app/actions/tarefas';
import { FormularioDeTarefaComEstado } from '@/app/components/FormularioDeTarefaComEstado';
export default async function HomePage() {
const tarefas = await getTarefas(); // Chama a Server Action para obter as tarefas
return (
<main className="container mx-auto p-4 max-w-2xl">
<h1 className="text-3xl font-bold mb-6 text-center">Minhas Tarefas</h1>
<div className="mb-8">
<FormularioDeTarefaComEstado />
</div>
<section className="border rounded shadow-md p-4">
<h2 className="text-2xl font-semibold mb-4">Lista de Tarefas</h2>
{tarefas.length === 0 ? (
<p className="text-gray-500">Nenhuma tarefa adicionada ainda.</p>
) : (
<ul className="space-y-3">
{tarefas.map(tarefa => (
<li key={tarefa.id} className="flex items-center justify-between p-3 border rounded-md bg-gray-50">
<div>
<h3 className="text-lg font-medium">{tarefa.titulo}</h3>
{tarefa.descricao && <p className="text-sm text-gray-600">{tarefa.descricao}</p>}
</div>
{/* Opcional: Botão de concluir tarefa aqui */}
{/* <form action={concluirTarefa.bind(null, tarefa.id)}>
<button type="submit" className="py-1 px-3 bg-green-500 text-white rounded-md text-sm">
Concluir
</button>
</form> */}
</li>
))}
</ul>
)}
</section>
</main>
);
}4. Resumo e Próximos Passos
Parabéns! 🎉 Você implementou um formulário completo e interativo usando Server Actions no Next.js 15.
Nesta aula, você aprendeu:
- Como Server Actions simplificam a lógica de formulários, permitindo código "full-stack".
- A usar
action={suaServerAction}para vincular um formulário a uma Server Action. - A gerenciar estados de carregamento com
useFormStatus. - A exibir mensagens de feedback do servidor com
useFormState. - A revalidar dados com
revalidatePathe redirecionar comredirect.
O que vem a seguir?
- Validação de Esquema: Para validações mais complexas, integre bibliotecas como Zod ou Yup diretamente em suas Server Actions.
- Otimizações: Explore como otimizar suas Server Actions para evitar re-renders desnecessários e melhorar a performance.
- Autenticação e Autorização: Como proteger suas Server Actions para que apenas usuários autenticados e autorizados possam executá-las.
- Tratamento de Erros Globais: Implemente estratégias mais robustas para capturar e exibir erros não tratados em suas Server Actions.
Continue praticando e explorando as possibilidades que as Server Actions oferecem para construir aplicações Next.js mais eficientes e elegantes! Até a próxima aula! 🚀