Next.js: Projeto Final: CRUD & Server Actions
Última atualização: 2026-08-26
O gerenciamento de tarefas está no coração de uma plataforma SaaS — criar, editar, excluir, arrastar e soltar para reordenar, fazer upload de arquivos, pesquisar e filtrar. Um sistema CRUD abrangente permite que os usuários gerenciem fluxos de trabalho de forma eficiente.
1. O Que Você Vai Aprender
- Implementar CRUD de Tarefas (criar / atualizar / excluir) usando Server Actions
- Validação Zod + modo de atualização otimista
useOptimistic useActionStatePop-up de Confirmação e Tratamento de Erros- Implementar ordenação por arrastar e soltar para alternar entre as colunas "A Fazer", "Em Andamento" e "Concluído"
- Vincular uploads de arquivos do Uploadthing ao modelo Attachment
- Parâmetros de busca URL + shadcn/ui DataTable para implementar pesquisa, filtragem e paginação
2. Uma História Real de um Gerente de Projeto
(1) Ponto de Dor: Uma equipe de 50 pessoas desperdiça 3 horas por dia em gerenciamento de tarefas
Bob é gerente de projeto na Acme Corp, que gerencia 5 projetos e mais de 200 tarefas. A equipe gasta uma quantidade significativa de tempo todos os dias: criando tarefas (preenchendo formulários de 10 campos repetidamente), arrastando e soltando para atualizar status (como colar Post-its em um quadro branco), pedindo arquivos aos colegas (trocando-os no WeChat) e procurando tarefas (rolando planilhas do Excel). Alice calculou que, em uma equipe de 50 pessoas, cada pessoa desperdiça 36 minutos por dia em gerenciamento ineficiente de tarefas, o que equivale a uma perda mensal de produtividade de 600 horas.
(2) Server Actions + Atualizações Otimistas + Uma Solução para Drag-and-Drop
Implemente CRUD sem refresh usando Server Actions + useOptimistic para feedback instantâneo + mudança de estado por arrastar e soltar.
// Uma linha de Server Action completa a criação da tarefa + Atualização de Cache
<form action={createTask}>
<input name="title" required />
<input name="projectId" type="hidden" value={project.id} />
<button type="submit">Criar Tarefa</button>
</form>
(3) Resultados
| Cenário | Antes da Otimização | Após Otimização | Melhoria de Eficiência |
|---|---|---|---|
| Criar Tarefa | 3 minutos (formulário completo) | 15 segundos (criação rápida) | 12x |
| Ordenação por Arrastar e Soltar | Quadro Branco Manuscrito | Clicar na Coluna de Status | Instantâneo |
| Compartilhamento de Arquivos | Enviar Arquivos pelo WeChat | Upload via Uploadthing | 5x |
| Pesquisar Tarefa | Navegando no Excel | Pesquisa em tempo real na DataTable | 20x |
| Produtividade Mensal | 600 horas desperdiçadas | ~50 horas desperdiçadas | Redução de 92% |
3. Server Actions para CRUD de Tarefas
(1) Arquitetura Operacional
graph TB
subgraph "Server Actions"
A[createTask] --> B[Validar com Zod]
B --> C[Criar com Prisma]
C --> D[revalidateTag]
D --> E[redirect]
end
subgraph "Client"
F[TaskForm] -->|action| A
G[useOptimistic] -->|UI instantânea| F
H[useActionState] -->|confirmar| J[deleteTask]
end
subgraph "Cache"
D --> K[revalidateTag<br/>"tasks"]
K --> L[Atualizar UI]
end
style A fill:#cce5ff
style G fill:#d4edda
style K fill:#ffeeba
(2) Comparação de Operações de Tarefa
| Operação | Server Action | Validação | Política de Cache | Feedback da UI |
|---|---|---|---|---|
| createTask | "use server" |
Zod | revalidateTag("tasks") |
useOptimistic |
| updateTask | "use server" |
Zod parcial | revalidateTag("tasks") |
Atualização Otimista |
| deleteTask | "use server" |
Verificação de Confirmação | revalidateTag("tasks") |
useActionState |
| reorderTask | "use server" |
Verificar Número | revalidateTag("tasks") |
Reordenar Imediatamente |
▶ Exemplo: Definição de Server Actions
Saída:
Diagrama: createTask; Validar com Zod; Criar com Prisma; revalidateTag; redirect; TaskForm.
// src/lib/actions/task-actions.ts
"use server"
import { revalidateTag } from "next/cache"
import { redirect } from "next/navigation"
import { z } from "zod"
import { prisma } from "@/lib/db"
import { auth } from "@/lib/auth"
import { TaskStatus, TaskPriority } from "@prisma/client"
const createTaskSchema = z.object({
title: z.string().min(1, "Título é obrigatório").max(200, "Título muito longo"),
description: z.string().max(2000).optional(),
projectId: z.string().min(1),
assigneeId: z.string().optional(),
priority: z.nativeEnum(TaskPriority).optional(),
dueDate: z.string().optional(),
})
const updateTaskSchema = z.object({
id: z.string().min(1),
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional(),
status: z.nativeEnum(TaskStatus).optional(),
priority: z.nativeEnum(TaskPriority).optional(),
assigneeId: z.string().nullable().optional(),
dueDate: z.string().nullable().optional(),
order: z.number().int().optional(),
})
export async function createTask(formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("Não autorizado")
}
const rawData = {
title: formData.get("title") as string,
description: formData.get("description") as string | undefined,
projectId: formData.get("projectId") as string,
assigneeId: formData.get("assigneeId") as string | undefined,
priority: (formData.get("priority") as TaskPriority) || "MEDIUM",
dueDate: formData.get("dueDate") as string | undefined,
}
const validated = createTaskSchema.safeParse(rawData)
if (!validated.success) {
return {
error: validated.error.flatten().fieldErrors,
message: "Falha na validação",
}
}
const data = validated.data
const maxOrder = await prisma.task.findFirst({
where: { projectId: data.projectId, status: "TODO" },
orderBy: { order: "desc" },
select: { order: true },
})
await prisma.task.create({
data: {
title: data.title,
description: data.description || null,
status: "TODO",
priority: data.priority || "MEDIUM",
projectId: data.projectId,
creatorId: session.user.id,
assigneeId: data.assigneeId || null,
dueDate: data.dueDate ? new Date(data.dueDate) : null,
order: (maxOrder?.order ?? -1) + 1,
},
})
revalidateTag("tasks")
revalidateTag(`project-${data.projectId}`)
}
export async function updateTask(formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("Não autorizado")
}
const rawData = {
id: formData.get("id") as string,
title: formData.get("title") as string | undefined,
description: formData.get("description") as string | undefined,
status: formData.get("status") as TaskStatus | undefined,
priority: formData.get("priority") as TaskPriority | undefined,
assigneeId: formData.get("assigneeId") as string | null | undefined,
dueDate: formData.get("dueDate") as string | null | undefined,
}
const validated = updateTaskSchema.safeParse(rawData)
if (!validated.success) {
return {
error: validated.error.flatten().fieldErrors,
message: "Falha na validação",
}
}
const data = validated.data
const updateData: Record<string, unknown> = {}
if (data.title !== undefined) updateData.title = data.title
if (data.description !== undefined) updateData.description = data.description
if (data.status !== undefined) updateData.status = data.status
if (data.priority !== undefined) updateData.priority = data.priority
if (data.assigneeId !== undefined) updateData.assigneeId = data.assigneeId
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate ? new Date(data.dueDate) : null
if (data.order !== undefined) updateData.order = data.order
await prisma.task.update({
where: { id: data.id },
data: updateData,
})
revalidateTag("tasks")
}
export async function deleteTask(prevState: { message: string }, formData: FormData) {
const session = await auth()
if (!session?.user?.id) {
return { message: "Não autorizado" }
}
const taskId = formData.get("id") as string
if (!taskId) {
return { message: "ID da tarefa é obrigatório" }
}
const task = await prisma.task.findUnique({
where: { id: taskId },
select: { creatorId: true, projectId: true },
})
if (!task) {
return { message: "Tarefa não encontrada" }
}
if (task.creatorId !== session.user.id) {
return { message: "Apenas o criador pode excluir esta tarefa" }
}
await prisma.task.delete({ where: { id: taskId } })
revalidateTag("tasks")
revalidateTag(`project-${task.projectId}`)
return { message: "Tarefa excluída com sucesso" }
}
export async function reorderTask(taskId: string, newStatus: TaskStatus, newOrder: number) {
const session = await auth()
if (!session?.user?.id) {
throw new Error("Não autorizado")
}
await prisma.task.update({
where: { id: taskId },
data: { status: newStatus, order: newOrder },
})
revalidateTag("tasks")
}
Saída:
Aceita campos do formulário: title, description, projectId, assigneeId, priority.
Valida a entrada com o schema Zod antes do processamento.
Grava no banco de dados e invalida as tags de cache relevantes.
4. Atualizações Otimistas e Pop-ups de Confirmação
(1) Três Modos de UI
graph LR
subgraph "Atualização Otimista"
A1[Usuário clica] --> B1[Atualização instantânea da UI]
B1 --> C1[Server Action]
C1 --> D1{Rollback?}
D1 -->|Sucesso| E1[Manter]
D1 -->|Erro| F1[Reverter]
end
subgraph "Action State"
A2[Usuário clica em excluir] --> B2[Mostrar diálogo de confirmação]
B2 --> C2[Se confirmado<br/>chamar deleteTask]
C2 --> D2[Mostrar mensagem de resultado]
end
subgraph "Tradicional"
A3[Enviar formulário] --> B3[Spinner de carregamento]
B3 --> C3[Server Action]
C3 --> D3[revalidateTag + refresh]
end
style B1 fill:#d4edda
style B2 fill:#ffeeba
style B3 fill:#f8d7da
▶ Exemplo: useOptimistic — Atualizações Otimistas
Saída:
Diagrama ilustra a arquitetura/fluxo descrito.
// src/components/tasks/task-list.tsx
"use client"
import { useOptimistic } from "react"
import { TaskCard } from "@/components/tasks/task-card"
import type { TaskWithAssignee } from "@/types"
interface TaskListProps {
tasks: TaskWithAssignee[]
status: "TODO" | "IN_PROGRESS" | "DONE"
onStatusChange: (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => void
}
export function TaskList({ tasks, status, onStatusChange }: TaskListProps) {
const [optimisticTasks, addOptimisticTask] = useOptimistic(
tasks,
(state, newTask: TaskWithAssignee) => {
return [newTask, ...state]
}
)
return (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{status.replace("_", " ")}
<span className="ml-2 text-xs">({optimisticTasks.length})</span>
</h3>
{optimisticTasks.length === 0 ? (
<div className="rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
Nenhuma tarefa
</div>
) : (
<div className="space-y-2">
{optimisticTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={onStatusChange}
/>
))}
</div>
)}
</div>
)
}
Saída:
Renderiza a UI do componente ▶ Exemplo: useOptimistic — Atualizações Otimistas conforme descrito na seção.
▶ Exemplo: useActionState para Confirmar Exclusão
Saída:
O componente renderiza a UI descrita no navegador.
// src/components/tasks/delete-task-button.tsx
"use client"
import { useActionState, useState } from "react"
import { deleteTask } from "@/lib/actions/task-actions"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
interface DeleteTaskButtonProps {
taskId: string
taskTitle: string
}
export function DeleteTaskButton({ taskId, taskTitle }: DeleteTaskButtonProps) {
const [open, setOpen] = useState(false)
const [state, formAction, isPending] = useActionState(deleteTask, {
message: "",
})
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</Button>
</DialogTrigger>
<DialogContent>
<form action={formAction}>
<input type="hidden" name="id" value={taskId} />
<DialogHeader>
<DialogTitle>Excluir Tarefa</DialogTitle>
<DialogDescription>
Tem certeza de que deseja excluir "{taskTitle}"? Esta ação não pode ser desfeita.
</DialogDescription>
</DialogHeader>
{state.message && state.message !== "Tarefa excluída com sucesso" && (
<div className="my-4 rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{state.message}
</div>
)}
{state.message === "Tarefa excluída com sucesso" && (
<div className="my-4 rounded-md bg-green-50 p-3 text-sm text-green-700">
{state.message}
</div>
)}
<DialogFooter className="mt-4">
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
>
Cancelar
</Button>
<Button
type="submit"
variant="destructive"
disabled={isPending}
>
{isPending ? "Excluindo..." : "Excluir"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
Saída:
Formulário interativo com gerenciamento de estado client-side.
Conteúdo visível: Excluir Tarefa
5. Ordenação por Arrastar e Soltar (Kanban de Três Colunas)
(1) Estrutura Kanban
graph LR
subgraph "Quadro de Tarefas"
A["A Fazer<br/>(Coluna)"] -->|Arrastar| B["Em Andamento<br/>(Coluna)"]
B -->|Arrastar| C["Concluído<br/>(Coluna)"]
C -->|Arrastar de volta| B
B -->|Arrastar de volta| A
end
style A fill:#fef3c7
style B fill:#dbeafe
style C fill:#d1fae5
▶ Exemplo: Layout de três colunas para um quadro de tarefas
Saída:
Diagrama da estrutura de layout e template no diretório app.
// src/components/tasks/task-board.tsx
"use client"
import { useCallback } from "react"
import { TaskList } from "@/components/tasks/task-list"
import { reorderTask } from "@/lib/actions/task-actions"
import type { TaskWithAssignee } from "@/types"
interface TaskBoardProps {
tasks: TaskWithAssignee[]
}
export function TaskBoard({ tasks }: TaskBoardProps) {
const todoTasks = tasks.filter((t) => t.status === "TODO")
const inProgressTasks = tasks.filter((t) => t.status === "IN_PROGRESS")
const doneTasks = tasks.filter((t) => t.status === "DONE")
const handleStatusChange = useCallback(
async (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => {
const targetList = tasks.filter((t) => t.status === newStatus)
const maxOrder = targetList.length > 0
? Math.max(...targetList.map((t) => t.order))
: 0
try {
await reorderTask(taskId, newStatus, maxOrder + 1)
} catch (error) {
console.error("Falha ao reordenar tarefa:", error)
}
},
[tasks]
)
return (
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={todoTasks}
status="TODO"
onStatusChange={handleStatusChange}
/>
</div>
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={inProgressTasks}
status="IN_PROGRESS"
onStatusChange={handleStatusChange}
/>
</div>
<div className="rounded-lg bg-muted/50 p-4">
<TaskList
tasks={doneTasks}
status="DONE"
onStatusChange={handleStatusChange}
/>
</div>
</div>
)
}
Saída:
Renderiza a UI do componente ▶ Exemplo: Layout de três colunas para um quadro de tarefas conforme descrito na seção.
▶ Exemplo: Componente Task Card
Saída:
O componente renderiza a UI descrita no navegador.
// src/components/tasks/task-card.tsx
"use client"
import { DeleteTaskButton } from "@/components/tasks/delete-task-button"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import type { TaskWithAssignee } from "@/types"
interface TaskCardProps {
task: TaskWithAssignee
onStatusChange: (taskId: string, newStatus: "TODO" | "IN_PROGRESS" | "DONE") => void
}
const statusActions: Record<string, ("TODO" | "IN_PROGRESS" | "DONE")[]> = {
TODO: ["IN_PROGRESS"],
IN_PROGRESS: ["TODO", "DONE"],
DONE: ["IN_PROGRESS"],
}
const priorityColors: Record<string, string> = {
LOW: "bg-gray-100 text-gray-700",
MEDIUM: "bg-blue-100 text-blue-700",
HIGH: "bg-orange-100 text-orange-700",
URGENT: "bg-red-100 text-red-700",
}
export function TaskCard({ task, onStatusChange }: TaskCardProps) {
const initials = task.assignee?.name
?.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
return (
<div className="group rounded-lg border bg-card p-3 shadow-sm transition-all hover:shadow-md">
<div className="flex items-start justify-between">
<h4 className="text-sm font-medium">{task.title}</h4>
<DeleteTaskButton taskId={task.id} taskTitle={task.title} />
</div>
{task.description && (
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{task.description}
</p>
)}
<div className="mt-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className={priorityColors[task.priority]}
>
{task.priority}
</Badge>
{task.dueDate && (
<span className="text-xs text-muted-foreground">
{new Date(task.dueDate).toLocaleDateString("pt-BR", {
month: "short",
day: "numeric",
})}
</span>
)}
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
{statusActions[task.status].map((action) => (
<button
key={action}
onClick={() => onStatusChange(task.id, action)}
className="rounded px-2 py-1 text-[10px] font-medium uppercase transition-colors hover:bg-accent"
>
{action === "TODO"
? "\u21A9"
: action === "IN_PROGRESS"
? "\u25B6"
: "\u2713"}
</button>
))}
</div>
{task.assignee && (
<Avatar className="h-6 w-6">
<AvatarImage src={task.assignee.image ?? undefined} />
<AvatarFallback className="text-[10px]">
{initials}
</AvatarFallback>
</Avatar>
)}
</div>
</div>
{task._count?.comments > 0 && (
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
{task._count.comments}
</div>
)}
</div>
)
}
Saída:
Renderiza a UI do componente ▶ Exemplo: Componente Task Card conforme descrito na seção.
6. Upload de Arquivos (uploadthing)
(1) Arquitetura de Upload
sequenceDiagram
participant C as Client
participant U as UploadThing
participant S as Server Action
participant DB as Banco de Dados
C->>U: Fazer upload do arquivo (multipart)
U-->>C: Retornar fileUrl + metadados
C->>S: Server Action (formData com fileUrl)
S->>DB: Criar registro Attachment
DB-->>S: Attachment criado
S->>S: revalidateTag("tasks")
S-->>C: Sucesso
▶ Exemplo: Configuração do UploadThing
Saída:
Diagrama de sequência mostrando o fluxo de mensagens entre os participantes.
// src/lib/uploadthing.ts
import { createUploadthing, type FileRouter } from "uploadthing/next"
import { auth } from "@/lib/auth"
const f = createUploadthing()
export const ourFileRouter = {
taskAttachment: f({
image: { maxFileSize: "4MB", maxFileCount: 5 },
pdf: { maxFileSize: "8MB", maxFileCount: 3 },
"text/*": { maxFileSize: "2MB", maxFileCount: 5 },
})
.middleware(async () => {
const session = await auth()
if (!session?.user) throw new Error("Não autorizado")
return { userId: session.user.id }
})
.onUploadComplete(async ({ metadata, file }) => {
return { uploadedBy: metadata.userId, fileUrl: file.url }
}),
} satisfies FileRouter
export type OurFileRouter = typeof ourFileRouter
Saída:
Define tipo(s) TypeScript: FileRouter, OurFileRouter.
// src/app/api/uploadthing/core.ts
import { createRouteHandler } from "uploadthing/next"
import { ourFileRouter } from "@/lib/uploadthing"
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
})
7. Pesquisa, Filtragem e Paginação
(1) Estrutura de Parâmetros de Busca URL
graph LR
A[Parâmetros de Busca URL] --> B[Server Component]
B --> C[Consulta Prisma]
C --> D[Resultados Filtrados]
D --> E[shadcn/ui DataTable]
E -->|Interação do usuário| F[Atualizar parâmetros URL]
F -->|Navegação Next.js| A
style A fill:#cce5ff
style E fill:#d4edda
style F fill:#ffeeba
8. Exemplo Completo: Página Completa do Quadro de Tarefas
// src/app/(dashboard)/dashboard/projects/[id]/page.tsx
import { Suspense } from "react"
import { prisma } from "@/lib/db"
import { auth } from "@/lib/auth"
import { notFound } from "next/navigation"
import { TaskBoard } from "@/components/tasks/task-board"
import { CreateTaskForm } from "@/components/tasks/create-task-form"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
interface ProjectBoardPageProps {
params: Promise<{ id: string }>
}
export default async function ProjectBoardPage({ params }: ProjectBoardPageProps) {
const { id } = await params
const session = await auth()
const project = await prisma.project.findUnique({
where: { id },
include: {
_count: { select: { tasks: true } },
},
})
if (!project) {
notFound()
}
const tasks = await prisma.task.findMany({
where: { projectId: id },
orderBy: { order: "asc" },
include: {
assignee: { select: { name: true, image: true } },
_count: { select: { comments: true, attachments: true } },
},
})
const statusCounts = {
TODO: tasks.filter((t) => t.status === "TODO").length,
IN_PROGRESS: tasks.filter((t) => t.status === "IN_PROGRESS").length,
DONE: tasks.filter((t) => t.status === "DONE").length,
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-3">
<div
className="h-4 w-4 rounded-full"
style={{ backgroundColor: project.color }}
/>
<h1 className="text-2xl font-bold">{project.name}</h1>
<Badge variant="secondary">
{project.status}
</Badge>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{tasks.length} tarefas · {statusCounts.TODO} a fazer ·{" "}
{statusCounts.IN_PROGRESS} em andamento · {statusCounts.DONE} concluídas
</p>
</div>
</div>
<Suspense fallback={<div className="h-12 animate-pulse rounded-lg bg-muted" />}>
<CreateTaskForm projectId={project.id} members={[]} />
</Suspense>
<TaskBoard tasks={tasks} />
</div>
)
}
Saída esperada (ao acessar /dashboard/projects/[id] no navegador):
→ Quadro Kanban de Três Colunas: A FAZER (8) | EM ANDAMENTO (5) | CONCLUÍDO (7)
→ Clique no botão "+" para abrir o Formulário de Criação Rápida → Digite um título → Enter → A tarefa aparece imediatamente na coluna A FAZER (Atualização Otimista)
→ Clique no botão ▶ do cartão da tarefa → Mover a tarefa para EM ANDAMENTO
→ Clique para Excluir → Exibir uma caixa de diálogo de confirmação → Confirmar Exclusão → A Tarefa Desapareceu
→ Arraste e solte arquivos na área de upload → Exibir uma barra de progresso → Anexos Associados às Tarefas
→ Pesquisar "auth" → Atualização do Parâmetro URL → DataTable Filtra para exibir tarefas correspondentes
❓ Perguntas Frequentes
P: Como escolher entre Server Actions e API Routes? R: Use Server Actions para envios de formulários e atualizações de cache (menos código, type-safe, sem necessidade de endpoints de API). Use API Routes para integrações de terceiros, webhooks e APIs mobile. Server Actions são a forma recomendada de lidar com formulários no Next.js, enquanto API Routes são usadas para clientes não-navegador.
P: Qual é a diferença entre
useOptimisticeuseStatecomum? R:useOptimisticé um novo hook introduzido no React 19, projetado especificamente para o padrão de atualização otimista usado com Server Actions. Ele atualiza a UI imediatamente enquanto a Server Action está sendo executada e reverte automaticamente para os dados reais se a Server Action falhar. UsaruseStatecom rollbacks manuais requer mais código boilerplate e é propenso a erros.
P: Qual é melhor, uploadthing ou upload local de arquivos? R: uploadthing fornece CDN, otimização de imagens e validação de segurança, sendo adequado para ambientes de produção. A solução local
/uploads/é fácil de implementar, mas não é adequada para produção (sem CDN, espaço de armazenamento limitado e difícil de fazer backup). O TaskFlow usa upload local como solução de desenvolvimento e muda para uploadthing no ambiente de produção.
P: Por que a paginação por parâmetros de busca URL não usa paginação client-side? R: A paginação por parâmetros de busca URL é "paginação verdadeira" (paginação server-side), que carrega apenas os dados da página atual e é adequada para grandes conjuntos de dados. A paginação client-side carrega todos os dados de uma vez, resultando em tempos de carregamento inicial lentos e desperdício de largura de banda. Como o TaskFlow pode conter dezenas de milhares de tarefas, a paginação server-side é a única opção viável.
P: Qual é o papel da validação Zod na Server Action? R: Ela fornece uma dupla camada de proteção — a validação no navegador (HTML5 required + validação front-end) previne erros acidentais, enquanto a validação Zod server-side previne requisições maliciosas. O Zod também oferece
safeParsetype-safe, que gera automaticamente mensagens de erro de tipo, sendo mais confiável do que validaçãoif-elseescrita manualmente.
P: Por que usar
useActionStatepara excluir uma tarefa em vez de chamardeleteTaskdiretamente? R:useActionStateé um hook projetado para Server Actions no React 19. Ele fornece estadoisPending(para desabilitar o botão e evitar envios duplicados) e uma função wrapperaction(para manipular automaticamenteformData). Comparado a uma chamada direta,useActionStateé mais seguro (proteção CSRF) e fornece feedback de UI mais natural.
📖 Resumo
- Server Actions: Use o comando
"use server"para executar funções diretamente no servidor, evitando rotas de API manuais - Zod fornece validação de formulários type-safe, que, quando combinada com o tratamento de erros das Server Actions, possibilita um fluxo de validação completo.
useOptimisticPermite atualizações de UI em tempo real e reverte automaticamente quando uma Server Action falhauseActionStateGerencia o status de pop-ups de confirmação, fornecendoisPendinge empacotamento automático de formData- O quadro Kanban de três colunas implementa mudança de estado por arrastar e soltar via Server Action
reorderTask - Uploadthing + Upload local
/uploads/como método duplo de upload de arquivos, suportando imagens, PDFs e texto - Parâmetros de busca URL permitem paginação verdadeira, pesquisa e filtragem; DataTable exibe informações completas de tarefas em 7 colunas
revalidateTag("tasks")Padroniza estratégias de invalidação de cache para garantir atualizações de UI em tempo real após operações CRUD
📝 Exercícios
-
Exercício Básico (⭐): Implemente um formulário de criação rápida de tarefas (Título + projectId) no projeto TaskFlow usando uma Server Action e uma atualização otimista. Verificação: Após clicar em "Criar", a tarefa aparece imediatamente na coluna A FAZER do quadro kanban.
-
Exercício Avançado (⭐⭐): Adicione um recurso de "Operação em Lote" à DataTable — após selecionar várias linhas, realize uma atualização em lote de seus status (A FAZER → EM ANDAMENTO) ou exclua-as em massa. Use
useOptimisticpara implementar atualizações otimistas para seleções em lote. Escreva uma Server ActionbulkUpdateTaskspara lidar com parâmetros de array. -
Desafio (⭐⭐⭐): Implemente ordenação completa por arrastar e soltar (usando
@dnd-kit/coreou a API nativa HTML5 Drag & Drop): Permita que os cartões de tarefa sejam livremente arrastados entre as três colunas; exiba um placeholder durante o arrasto; chamereorderTaskapós soltar o cartão para atualizar o status e a ordem de classificação; useuseOptimisticpara fornecer feedback imediato de ordenação após o arrasto; lide com conflitos de arrasto (bloqueio otimista quando dois usuários arrastam a mesma tarefa ao mesmo tempo).