Fundamentos do Next.js 15

0/26 aulas0%
pratica

Implementando Formulários Completos com Server Actions

Aprenda sobre implementando formulários completos com server actions

50 min
Aula 2 de 5

🚀 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) com useFormStatus.
  • 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
// 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
// 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
// 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
// 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:

  1. A Server Action que você quer usar.
  2. 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
// 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
// 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 usando fetch com next.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.tsx para começar do zero.
  • Crie o Módulo de Server Actions:
    • Crie um arquivo app/actions/tarefas.ts.
    • Adicione a Server Action adicionarTarefaComEstado e a função getTarefas conforme os exemplos acima.
    • Certifique-se de incluir a linha "use server"; no topo do arquivo.
  • Crie o Componente BotaoDeSubmissao:
    • Crie um arquivo app/components/BotaoDeSubmissao.tsx.
    • Implemente o componente que usa useFormStatus para desabilitar o botão e mudar o texto durante a submissão. Lembre-se de "use client";.
  • Crie o Componente FormularioDeTarefaComEstado:
    • Crie um arquivo app/components/FormularioDeTarefaComEstado.tsx.
    • Implemente o formulário completo usando useFormState para exibir mensagens de sucesso/erro e o BotaoDeSubmissao. Loremn-se de "use client";.
  • Exiba a Lista de Tarefas na Página Principal:
    • Modifique app/page.tsx para 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.
  • 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.
  • (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 prop formAction em um <button type="submit"> ou um <form action={...}> com um campo hidden para o ID).
    • concluirTarefa deve atualizar o status da tarefa e revalidar a página.
app/page.tsx (Exemplo de Estrutura)
// 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 revalidatePath e redirecionar com redirect.

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! 🚀

© 2025 Escola All Dev. Todos os direitos reservados.

Implementando Formulários Completos com Server Actions - Fundamentos do Next.js 15 | escola.all.dev.br