Next.js: Server Actions Avançado

Última atualização: 2026-08-26

Quando Server Actions encontram upload de arquivos e atualizações otimistas — os usuários recebem "feedback instantâneo" em vez de "ícones de carregamento girando".

1. O Que Você Vai Aprender



2. Uma História Real de uma Engenheira Front-End

(1) Ponto de Dor: Uploads de arquivos levam 8 segundos, fazendo os usuários pensarem que a página travou

Diana está desenvolvendo o recurso "Upload de Anexos" para o TaskFlow. Depois que um usuário seleciona uma imagem de 5 MB:

Para piorar, não há feedback visual durante esses 6 segundos — os usuários pensam que a página travou e continuam clicando no botão de enviar.

(2) A Solução com Server Actions

Use useOptimistic para exibir imediatamente anexos "virtuais" na UI + processamento de upload unificado via Server Action + tratamento de erros com try-catch.

TSX
// app/tasks/[id]/Attachments.tsx — Atualização Otimista + Upload de Arquivo
'use client'
import { useOptimistic, useActionState } from 'react'
import { uploadAttachment } from './actions'

export function Attachments({ taskId, initialFiles }: Props) {
  const [optimisticFiles, addOptimistic] = useOptimistic(
    initialFiles,
    (state, newFile: File) => [...state, { id: 'pending', name: newFile.name, url: URL.createObjectURL(newFile), status: 'uploading' }]
  )

  const [error, formAction, pending] = useActionState(uploadAttachment, null)

  return (
    <form action={formAction}>
      <input type="hidden" name="taskId" value={taskId} />
      <input type="file" name="file" onChange={e => {
        const file = e.target.files?.[0]
        if (file) addOptimistic(file)  // Exibir imediatamente na UI
      }} />
      <button type="submit" disabled={pending}>Enviar</button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <ul>{optimisticFiles.map(f => <li key={f.id}>{f.name} {f.status === 'uploading' ? '⏳' : '✅'}</li>)}</ul>
    </form>
  )
}

(3) Ganhos

Dimensão API Route Tradicional Server Action
Latência Percebida 6 segundos (sem feedback) Instantâneo (atualização otimista)
Número de linhas de código 120 linhas (API + cliente + validação) 45 linhas
Tratamento de Erros Requer try-catch manual em toda a cadeia Centralizado com useActionState
Verificação de Tamanho de Arquivo Cliente + Servidor Separadamente Consolidado na Server Action


3. Três Modos de Tratamento de Erros

(1) Padrão useActionState (Recomendado)

TSX
// app/error-demo/use-action-state.tsx
'use client'
import { useActionState } from 'react'

async function submitOrder(prevState: any, formData: FormData) {
  'use server'
  try {
    const quantity = Number(formData.get('quantity'))
    if (quantity < 1) throw new Error('Quantidade deve ser >= 1')
    if (quantity > 100) throw new Error('Quantidade excede o limite')

    await db.order.create({ data: { quantity } })
    revalidateTag('orders')
    return { success: true, message: 'Pedido criado' }
  } catch (err: any) {
    return { success: false, error: err.message }
  }
}

export default function OrderForm() {
  const [state, action, pending] = useActionState(submitOrder, null)

  return (
    <form action={action}>
      <input type="number" name="quantity" min={1} disabled={pending} />
      <button type="submit" disabled={pending}>{pending ? 'Enviando...' : 'Enviar'}</button>
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state?.success && <p style={{ color: 'green' }}>{state.message}</p>}
    </form>
  )
}

(2) Padrão startTransition

TSX
// app/error-demo/transition.tsx
'use client'
import { useTransition } from 'react'
import { submitFeedback } from './actions'

export function FeedbackForm() {
  const [isPending, startTransition] = useTransition()

  return (
    <form onSubmit={e => {
      e.preventDefault()
      const form = e.currentTarget
      const data = new FormData(form)
      startTransition(async () => {
        try {
          await submitFeedback(data)
          form.reset()
        } catch (err) {
          alert(`Erro: ${err}`)
        }
      })
    }}>
      <textarea name="feedback" required disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Enviando...' : 'Enviar Feedback'}
      </button>
    </form>
  )
}

(3) Padrão try-catch (dentro da Server Action)

TS
// app/actions/robust-action.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function robustAction(formData: FormData) {
  const id = formData.get('id') as string

  // 1. Validação de Parâmetros
  if (!id || isNaN(Number(id))) {
    throw new Error('ID inválido')
  }

  // 2. Lógica de Negócio + Tratamento de Erros
  try {
    await db.item.update({ where: { id: Number(id) }, data: { status: 'processed' } })
    revalidatePath('/items')
    return { ok: true }
  } catch (dbError) {
    console.error('Erro no banco de dados:', dbError)
    throw new Error('Falha ao atualizar item no banco de dados')
  }
}

(1) ▶ Exemplo: Comparação dos Três Padrões de Tratamento de Erros (Dificuldade: ⭐⭐)

Saída:

TEXT 📖 Somente leitura
Server action executes and calls revalidatePath() to refresh the page cache.
TSX
// app/error-comparison/page.tsx
import { revalidatePath } from 'next/cache'

// Padrão 1: try-catch dentro da Server Action + Valor de Retorno
async function createItem(formData: FormData) {
  'use server'
  try {
    const name = formData.get('name') as string
    if (!name || name.length < 2) return { error: 'Nome muito curto' }
    await fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', body: JSON.stringify({ title: name }) })
    revalidatePath('/error-comparison')
    return { success: true }
  } catch (err) {
    return { error: 'Erro de rede' }
  }
}

export default function ErrorComparisonPage() {
  return (
    <div style={{ display: 'grid', gap: 32, padding: 24 }}>
      <section>
        <h2>Padrão 1: Server Action retorna estado</h2>
        <form action={createItem}>
          <input name="name" required />
          <button type="submit">Criar</button>
        </form>
      </section>
    </div>
  )
}

Saída:

TEXT 📖 Somente leitura
Form fields: name. On submit, processes data and refreshes the page cache.
Visible content: Padrão 1: Server Action retorna estado | Criar


4. useOptimistic: Atualizações Otimistas

useOptimistic é um novo hook no React 19 que permite atualizar imediatamente a UI antes que uma server action seja concluída, e então reverter ou confirmar automaticamente a atualização quando o resultado real for retornado.

100%
sequenceDiagram
    participant User
    participant UI as UI do Cliente
    participant SA as Server Action
    participant DB as Banco de Dados

    User->>UI: Clica "Concluir Tarefa"
    UI->>UI: useOptimistic → Fica cinza imediatamente + marca de seleção
    UI->>SA: Chama Server Action
    SA->>DB: Atualiza o banco de dados
    DB-->>SA: Sucesso
    SA-->>UI: Retorna Resultados
    UI->>UI: Comparação com a Situação Real → Confirmar ou Reverter
Status Exibição na UI Dado Real Percepção do Usuário
Atualização de Status ✅ Concluída (marca de seleção) ⏳ Em Andamento Imediata
Confirmação do Servidor ✅ Concluída ✅ Concluída Sem alteração
Servidor Rejeitou ❌ Incompleta (Revertida) ❌ Incompleta Animação de Reversão

(2) ▶ Exemplo: Atualização Otimista do Status da Tarefa (Dificuldade ⭐⭐⭐)

Saída:

TEXT 📖 Somente leitura
Sequence diagram showing message flow between participants.
TSX
// app/todos/optimistic-list.tsx
'use client'
import { useOptimistic } from 'react'
import { revalidatePath } from 'next/cache'

type Todo = { id: number; title: string; completed: boolean }

export function OptimisticTodoList({ todos: initialTodos }: { todos: Todo[] }) {
  const [todos, addOptimistic] = useOptimistic(
    initialTodos,
    (state, updatedTodo: Todo) =>
      state.map(t => t.id === updatedTodo.id ? { ...t, completed: !t.completed } : t)
  )

  return (
    <ul>{todos.map(todo => (
      <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.title}
        <form action={async (formData: FormData) => {
          'use server'
          const id = Number(formData.get('id'))
          await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            method: 'PATCH', body: JSON.stringify({ completed: true })
          })
          revalidatePath('/todos/optimistic')
        }} onSubmit={e => {
          // Atualização Otimista: Atualizar imediatamente antes de enviar o formulário
          addOptimistic({ ...todo, completed: !todo.completed })
        }}>
          <input type="hidden" name="id" value={todo.id} />
          <button type="submit">{todo.completed ? 'Desfazer' : 'Concluir'}</button>
        </form>
      </li>
    ))}</ul>
  )
}

Saída:

TEXT 📖 Somente leitura
On submit, processes data and refreshes the page cache.


5. Processamento de Upload de Arquivos

Server Actions podem receber arquivos diretamente via formData. O servidor processa o fluxo do arquivo e suporta verificação de tamanho, verificação de tipo e processamento de armazenamento.

(1) Processamento de Arquivos no Servidor

TS
// app/actions/upload.ts
'use server'
import { revalidateTag } from 'next/cache'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'

const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']
const MAX_SIZE = 5 * 1024 * 1024  // 5 MB

export async function uploadAvatar(prevState: any, formData: FormData) {
  const file = formData.get('avatar') as File | null
  if (!file) return { error: 'Nenhum arquivo selecionado' }

  // Validação de Tipo
  if (!ALLOWED_TYPES.includes(file.type)) {
    return { error: 'Tipo de arquivo inválido. Permitidos: JPEG, PNG, WebP, PDF' }
  }

  // Verificação de Tamanho
  if (file.size > MAX_SIZE) {
    return { error: `Arquivo muito grande. Tamanho máximo: ${MAX_SIZE / 1024 / 1024} MB` }
  }

  try {
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
    const filename = `${Date.now()}-${file.name.replace(/\s/g, '_')}`
    const path = join('public/uploads', filename)
    await writeFile(path, buffer)

    // Salvar no banco de dados
    await db.avatar.create({ data: { filename, path: `/uploads/${filename}` } })
    revalidateTag('avatars')
    return { success: true, url: `/uploads/${filename}` }
  } catch (err) {
    return { error: 'Falha ao enviar arquivo' }
  }
}

(2) Envio do formulário no lado do cliente

TSX
// app/profile/avatar-upload.tsx
'use client'
import { useActionState } from 'react'
import { uploadAvatar } from '@/app/actions/upload'

export function AvatarUpload() {
  const [state, action, pending] = useActionState(uploadAvatar, null)

  return (
    <form action={action}>
      <input type="file" name="avatar" accept="image/jpeg,image/png,image/webp" disabled={pending} />
      <button type="submit" disabled={pending}>
        {pending ? 'Enviando...' : 'Enviar Avatar'}
      </button>
      {state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
      {state?.success && state?.url && (
        <div>
          <p style={{ color: 'green' }}>Upload bem-sucedido!</p>
          <img src={state.url} alt="avatar" style={{ width: 100, height: 100, borderRadius: '50%' }} />
        </div>
      )}
    </form>
  )
}

(3) Upload de Múltiplos Arquivos

TSX
// app/actions/multi-upload.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function uploadGallery(prevState: any, formData: FormData) {
  const files = formData.getAll('photos') as File[]
  if (files.length === 0) return { error: 'Nenhum arquivo selecionado' }
  if (files.length > 10) return { error: 'Máximo de 10 arquivos permitidos' }

  const uploaded: string[] = []
  for (const file of files) {
    if (file.size > 5 * 1024 * 1024) continue
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
    const filename = `${Date.now()}-${file.name}`
    await writeFile(join('public/uploads', filename), buffer)
    uploaded.push(`/uploads/${filename}`)
  }

  revalidateTag('gallery')
  return { success: true, urls: uploaded }
}

(3) ▶ Exemplo: Upload Completo de Arquivo + Visualização (Dificuldade: ⭐⭐⭐)

Saída:

TEXT 📖 Somente leitura
Renders the uploadGallery component UI.
TSX
// app/upload-demo/page.tsx — Visão Geral do Upload de Arquivos
import { revalidatePath } from 'next/cache'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import fs from 'node:fs'

export default async function UploadDemoPage() {
  // Ler a lista de arquivos existentes
  const uploadDir = join(process.cwd(), 'public', 'uploads')
  const files = fs.existsSync(uploadDir) ? fs.readdirSync(uploadDir) : []

  return (
    <div style={{ maxWidth: 600, margin: '0 auto', padding: 24 }}>
      <h1>Demonstração de Upload de Arquivos</h1>

      <form action={async (formData: FormData) => {
        'use server'
        const file = formData.get('file') as File
        if (!file) return

        if (file.size > 2 * 1024 * 1024) {
          return { error: 'Arquivo muito grande (máx 2MB)' }
        }

        const bytes = await file.arrayBuffer()
        const buffer = Buffer.from(bytes)
        const filename = `${Date.now()}-${file.name}`
        await writeFile(join('public/uploads', filename), buffer)
        revalidatePath('/upload-demo')
      }}>
        <input type="file" name="file" required />
        <button type="submit">Enviar</button>
      </form>

      <div style={{ marginTop: 24 }}>
        <h2>Arquivos Enviados ({files.length})</h2>
        <ul>{files.map(f => (
          <li key={f}>
            <a href={`/uploads/${f}`} target="_blank">{f}</a>
          </li>
        ))}</ul>
      </div>
    </div>
  )
}

Saída:

TEXT 📖 Somente leitura
On submit, processes data and refreshes the page cache.
Visible content: Demonstração de Upload de Arquivos


6. Transação: Escrita no BD + Invalidação de Cache + Redirecionamento

Server actions em nível de produção geralmente consistem em três etapas: escrever no banco de dados → limpar o cache → redirecionar o usuário.

100%
sequenceDiagram
    participant User
    participant Action as Server Action
    participant DB as Banco de Dados
    participant Cache as Camada de Cache

    User->>Action: Envia Formulário
    Action->>Action: Validação Zod
    Action->>DB: INSERT / UPDATE
    DB-->>Action: Sucesso
    Action->>Cache: revalidateTag / revalidatePath
    Action->>User: redirect para uma nova página/Página de Detalhes
Etapa API Descrição
Escrita db.create() / db.update() Operações de Banco de Dados com Prisma / Drizzle
Expiração de Cache revalidateTag() / revalidatePath() Garante que a página de lista exiba os dados mais recentes
Redirecionamento redirect() Redireciona para a página de detalhes ou lista após o envio

(4) ▶ Exemplo: Modo de Transação Completo (Dificuldade: ⭐⭐⭐)

Saída:

TEXT 📖 Somente leitura
Sequence diagram showing message flow between participants.
TSX
// app/posts/create/page.tsx — Escrita + Expiração de Cache + Redirecionamento
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(5).max(200),
  content: z.string().min(20),
  published: z.coerce.boolean().default(false),
})

export default function CreatePostPage() {
  return (
    <form action={async (formData: FormData) => {
      'use server'
      // 1. Validação
      const validated = postSchema.safeParse({
        title: formData.get('title'),
        content: formData.get('content'),
        published: formData.get('published'),
      })
      if (!validated.success) return { errors: validated.error.flatten().fieldErrors }

      // 2. Escrever no banco de dados
      const post = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        body: JSON.stringify({
          title: validated.data.title,
          body: validated.data.content,
          userId: 1,
        })
      }).then(r => r.json())

      // 3. Expiração de Cache
      revalidateTag('posts')
      revalidatePath('/posts')

      // 4. Redirecionar para o novo artigo
      redirect(`/posts/${post.id}`)
    }}>
      <div><input name="title" required placeholder="Título do post" /></div>
      <div><textarea name="content" required placeholder="Conteúdo" rows={10} /></div>
      <div><label><input name="published" type="checkbox" value="true" /> Publicado</label></div>
      <button type="submit">Criar Post</button>
    </form>
  )
}

(5) ▶ Exemplo: startTransition + Tratamento de Erros no Cliente (Dificuldade ⭐⭐⭐)

Saída:

TEXT 📖 Somente leitura
On submit, processes data server-side and redirects.
TSX
// app/newsletter/page.tsx
'use client'
import { useTransition, useState } from 'react'
import { revalidateTag } from 'next/cache'

async function subscribeNewsletter(formData: FormData) {
  'use server'
  const email = formData.get('email') as string
  if (!email || !email.includes('@')) throw new Error('Email inválido')
  await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    body: JSON.stringify({ title: email, body: 'Assinatura de newsletter' })
  })
  revalidateTag('newsletter')
}

export default function NewsletterForm() {
  const [isPending, startTransition] = useTransition()
  const [error, setError] = useState<string | null>(null)

  return (
    <form onSubmit={e => {
      e.preventDefault()
      const form = e.currentTarget
      const data = new FormData(form)
      setError(null)
      startTransition(async () => {
        try {
          await subscribeNewsletter(data)
          form.reset()
        } catch (err: any) {
          setError(err.message)
        }
      })
    }}>
      <input type="email" name="email" required placeholder="seu@email.com" disabled={isPending} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Assinando...' : 'Assinar'}
      </button>
      {error && <p style={{ color: 'red' }}>Erro: {error}</p>}
    </form>
  )
}


7. Server Actions vs API Routes — Seleção

Dimensão Server Actions API Routes
Método de Chamada Protocolo RSC Payload HTTP (REST)
Cenários Aplicáveis Formulários próprios, Ações do Usuário API de Terceiros, Webhook, Mobile
Proteção CSRF ✅ Integrada ❌ Deve ser feito manualmente
Melhoria Progressiva ✅ Suportada ❌ Requer JS
Segurança de Tipos ✅ Tipos TS Completos ⚠️ Processamento manual
Upload de Arquivos ✅ Processamento Direto com formData ✅ Processamento de Stream via req
Métodos de Autenticação Função auth() Middleware / JWT
Limitação de Taxa ⚠️ Deve ser implementado manualmente ✅ Processamento Centralizado via Middleware
Expiração de Cache ✅ revalidate integrado ✅ Pode chamar revalidateTag
Dificuldade de Depuração Baixa (Chamada de Função) Média (Depuração HTTP)

(6) ▶ Recomendações de Seleção

Saída:

TEXT 📖 Somente leitura
Completed.
100%
graph TB
    A[Que tipo de interface é necessária?] --> B{Quem é o chamador?}
    B -->|Chamado diretamente pelo navegador| C{Existe um formulário?}
    B -->|Terceiros / Webhook / Mobile| D[API Route]
    C -->|Formulário| E[Server Action ✅]
    C -->|Não, interação JSON| F{Precisa fazer upload de arquivo?}
    F -->|Sim| E
    F -->|Não| D


8. Exemplo Completo: Detalhes da Tarefa com Atualizações Otimistas + Upload de Arquivos

TSX
// app/tasks/[id]/page.tsx — Server Actions Avançado — Abordagem Completa
import { revalidateTag } from 'next/cache'
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { z } from 'zod'

// ======== Esquemas ========
const commentSchema = z.object({
  content: z.string().min(1).max(1000),
  author: z.string().min(2).max(100),
})

// ======== Página de Detalhes da Tarefa ========
export default async function TaskDetailPage({ params }: { params: { id: string } }) {
  const task = await fetch(`https://jsonplaceholder.typicode.com/todos/${params.id}`).then(r => r.json())
  const comments = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}/comments`, {
    next: { tags: [`comments-${params.id}`] }
  }).then(r => r.json())

  return (
    <div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>
      <h1>{task.title}</h1>
      <p>Status: {task.completed ? '✅ Concluída' : '⏳ Pendente'}</p>

      {/* Alternar estado rapidamente */}
      <form action={async (formData: FormData) => {
        'use server'
        const id = Number(formData.get('id'))
        const completed = formData.get('completed') === 'true'
        await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
          method: 'PATCH',
          body: JSON.stringify({ completed: !completed })
        })
        revalidateTag(`task-${id}`)
      }}>
        <input type="hidden" name="id" value={params.id} />
        <input type="hidden" name="completed" value={String(task.completed)} />
        <button type="submit">{task.completed ? 'Marcar Pendente' : 'Marcar Concluída'}</button>
      </form>

      {/* Adicionar um comentário */}
      <h2>Comentários</h2>
      <form action={async (formData: FormData) => {
        'use server'
        const validated = commentSchema.safeParse({
          content: formData.get('content'),
          author: formData.get('author'),
        })
        if (!validated.success) return { errors: validated.error.flatten().fieldErrors }
        await fetch('https://jsonplaceholder.typicode.com/comments', {
          method: 'POST',
          body: JSON.stringify({
            postId: Number(params.id),
            name: validated.data.author,
            body: validated.data.content,
            email: 'user@example.com',
          })
        })
        revalidateTag(`comments-${params.id}`)
      }}>
        <div><input name="author" required placeholder="Seu nome" /></div>
        <div><textarea name="content" required placeholder="Comentário" /></div>
        <button type="submit">Adicionar Comentário</button>
      </form>

      <ul>{comments.map((c: any) => (
        <li key={c.id}><strong>{c.name}:</strong> {c.body}</li>
      ))}</ul>

      {/* Upload de Arquivos */}
      <h2>Anexos</h2>
      <form action={async (formData: FormData) => {
        'use server'
        const file = formData.get('file') as File
        if (!file || file.size === 0) return { error: 'Nenhum arquivo' }
        if (file.size > 5 * 1024 * 1024) return { error: 'Arquivo muito grande' }
        const bytes = await file.arrayBuffer()
        await writeFile(join('public/uploads', `${Date.now()}-${file.name}`), Buffer.from(bytes))
        revalidateTag(`attachments-${params.id}`)
      }}>
        <input type="file" name="file" />
        <button type="submit">Enviar</button>
      </form>
    </div>
  )
}

❓ Perguntas Frequentes

P: useOptimistic e useActionState podem ser usados juntos? R: Sim. useOptimistic é usado para atualizar a UI antecipadamente, enquanto useActionState é usado para recuperar o estado final realmente retornado pelo servidor. Um padrão comum é usar useOptimistic para gerenciar mudanças imediatas na UI e useActionState para confirmar ou reverter após receber a resposta do servidor.

P: Quais tipos de arquivo são suportados para upload em Server Actions? R: Qualquer tipo de arquivo suportado pelos navegadores é aceito. O servidor pode verificar o arquivo lendo file.type (tipo MIME) e file.size (número de bytes). A abordagem recomendada é salvar o arquivo no sistema de arquivos ou armazenamento em nuvem (S3/R2) e armazenar uma referência de caminho no banco de dados.

P: Como o redirect() é usado em uma Server Action? R: redirect() deve ser importado de next/navigation. Quando chamado em uma Server Action, ele lança uma exceção especial de redirecionamento. Deve ser chamado fora de um bloco try-catch — se colocado dentro de um bloco try, será capturado pelo bloco catch. O padrão correto é: validar → escrever → revalidar → redirecionar (não dentro de um bloco try).

P: Qual é o tempo limite para Server Actions? R: Plano Gratuito Vercel: 10 segundos (Serverless Function), 60 segundos (Pro). Self-hosted: Determinado pela configuração do seu servidor Node.js (ilimitado por padrão). Para operações de longa duração (como processamento de vídeo), recomendamos dividi-las em tarefas assíncronas com callbacks de Webhook, em vez de esperar sincronamente dentro de uma Server Action.

P: Uma Server Action pode chamar a função set-cookie várias vezes? R: Sim. Use a função cookies() para definir o cabeçalho de resposta: const cookieStore = cookies(); cookieStore.set('theme', 'dark'). No entanto, observe que as operações de cookie em uma Server Action entram em vigor em lote; você não pode definir um cookie e lê-lo dentro da mesma action.

P: Em quais cenários você deve usar uma Server Action em vez de uma API Route? R: Existem três cenários onde Server Actions são preferíveis: ① Ações de usuário próprias (envios de formulário, cliques de botão); ② Situações que exigem melhoria progressiva (ainda utilizável mesmo quando o JavaScript está desabilitado); ③ Integração estreita com o sistema de cache RSC (revalidateTag/revalidatePath). As vantagens das API Routes são: ① Integrações de terceiros (aplicativos mobile, webhooks); ② Sem necessidade de contexto de página HTML; ③ Requisito de APIs RESTful padrão.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie um arquivo app/quick-todo/page.tsx e use uma Server Action inline para adicionar e excluir tarefas. Na Server Action, use um bloco try-catch para tratar erros e retorne { error: string } para o cliente exibir.

  2. Exercício Avançado (⭐⭐): Construa uma página app/gallery/page.tsx de upload de múltiplas imagens que suporte a seleção de até 5 imagens por vez. Na Server Action, valide o tipo de arquivo (apenas imagens) e o tamanho (≤ 2MB por imagem), e exiba uma lista de miniaturas após o upload. Implemente armazenamento com writeFile e atualização com revalidatePath na Server Action.

  3. Desafio (⭐⭐⭐): Implemente uma página de "Quadro de Tarefas" que inclua três colunas de status (A Fazer / Em Andamento / Concluído). Use useOptimistic para implementar atualizações de UI em tempo real ao alternar status via arrastar e soltar (não é necessário arrastar de verdade; use botões para alternar). Após uma Server Action ser escrita, atualize automaticamente todas as colunas com revalidateTag. Adicione um recurso de upload de arquivos a cada cartão de tarefa.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%