Aprenda / Frontend — Tailwind & Next.js

FundamentosAula 10 de 2025 min0% da trilha

Projeto guiado: tela CRUD do zero

Junta as 9 lições anteriores numa única tela: listar, criar e validar tarefas.

Chegou a hora de juntar tudo: Tailwind pro visual, App Router pra rota, Server Component pra buscar dado, Client Component isolado só onde há interação, e formulário validado com RHF + Zod. O projeto: uma lista de tarefas com criação.

1. A rota e o Server Component que busca os dados

app/tarefas/page.tsx
import { NovaTarefaForm } from "./nova-tarefa-form";

async function getTarefas() {
  const res = await fetch("https://api.exemplo.com/tarefas", { cache: "no-store" });
  return res.json() as Promise<{ id: string; titulo: string; feita: boolean }[]>;
}

export default async function TarefasPage() {
  const tarefas = await getTarefas();

  return (
    <div className="max-w-lg mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Tarefas</h1>

      <NovaTarefaForm />

      <ul className="mt-6 flex flex-col gap-2">
        {tarefas.map((tarefa) => (
          <li
            key={tarefa.id}
            className={`border rounded-lg px-4 py-2 ${tarefa.feita ? "bg-slate-50 text-slate-400 line-through" : ""}`}
          >
            {tarefa.titulo}
          </li>
        ))}
      </ul>
    </div>
  );
}

2. O formulário — o único pedaço interativo

app/tarefas/nova-tarefa-form.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useRouter } from "next/navigation";

const schema = z.object({
  titulo: z.string().min(3, "Mínimo de 3 caracteres"),
});
type FormValues = z.infer<typeof schema>;

export function NovaTarefaForm() {
  const router = useRouter();
  const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });

  async function onSubmit(data: FormValues) {
    await fetch("https://api.exemplo.com/tarefas", {
      method: "POST",
      body: JSON.stringify(data),
    });
    reset();
    router.refresh(); // reexecuta o Server Component da page, sem recarregar a página inteira
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex gap-2">
      <input
        {...register("titulo")}
        placeholder="Nova tarefa"
        className="flex-1 border rounded-lg px-3 py-2"
      />
      <button disabled={isSubmitting} className="bg-blue-600 text-white px-4 py-2 rounded-lg disabled:opacity-50">
        Adicionar
      </button>
      {errors.titulo && <p className="text-sm text-red-600">{errors.titulo.message}</p>}
    </form>
  );
}

O ponto central: TarefasPage é async e busca dado direto (lição 8); NovaTarefaForm é o único componente marcado "use client" (lição 7), porque é o único que precisa de estado de formulário e evento de clique; e router.refresh() — em vez de window.location.reload() — reexecuta só o Server Component, sem perder o estado do resto da página.

Exercício

Adicione um botão "Concluir" em cada item da lista que chama PATCH /tarefas/:id marcando feita: true, e depois de responder, router.refresh(). Em qual componente esse botão precisa morar — no page.tsx ou num novo Client Component?

Quiz rápido

Por que só o formulário (NovaTarefaForm) precisa de "use client" nesta tela?
O que router.refresh() faz que window.location.reload() não faz?