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
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
"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?