Next.js: Busca de Dados: fetch & RSC

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

No RSC, o fetch não é mais apenas uma busca do navegador — ele estende a camada de cache, permitindo que você controle o ciclo de vida dos dados de forma declarativa.

1. O Que Você Vai Aprender



2. Uma História Real de um Desenvolvedor Full-Stack

(1) Ponto de Dor: O dashboard leva 8 segundos para carregar

Bob é o Líder Técnico da equipe TaskFlow. A página Dashboard precisa carregar cinco fontes de dados: estatísticas de usuários, número total de projetos, tarefas recentes, logs de atividade e notificações do sistema. O código inicial usava cinco chamadas seriais await fetch(...), cada uma esperando a anterior terminar — resultando em um tempo total de 2,1s + 1,8s + 1,5s + 0,9s + 1,7s = 8 segundos. Os usuários reclamavam que a página "demorava muito para carregar". Para piorar, a API era consultada novamente a cada atualização, fazendo a carga do banco de dados disparar para 5.000 QPS.

(2) A Solução com fetch do Next.js

Use Promise.all() para requisições paralelas + next: { revalidate: 60 } para cache de 60 segundos.

TSX
// app/dashboard/page.tsx
export default async function DashboardPage() {
  const [users, projects, tasks, logs, notifs] = await Promise.all([
    fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
    fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
    fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
    fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
  ]).then(responses => Promise.all(responses.map(r => r.json())))

  return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}

(3) Resultados

Dimensão Antes da Otimização Depois da Otimização
Tempo de carregamento 8 segundos (serial) 2,1 segundos (paralelo)
QPS do Banco de Dados 5.000 83 (cache de 60s)
Reclamações de Usuários 12 por dia 0
Linhas de código 35 linhas (5 fetches separados) 10 linhas


3. Os Três Modos de Cache do fetch

O Next.js 16 estende a API Web fetch adicionando três modos de cache. Todos os fetch no RSC usam force-cache (cache automático) por padrão, a menos que outro modo seja explicitamente especificado.

100%
graph LR
    A[RSC fetch] --> B{Modo de Cache}
    B --> C[force-cache<br/>Valor padrão]
    B --> D[no-store<br/>A cada atualização da página]
    B --> E[revalidate:N<br/>Janela de Tempo]
    C --> F[Cache de Dados<br/>Armazenamento Persistente]
    D --> G[Dados em Tempo Real<br/>Sem cache]
    E --> H[N segundos em cache<br/>Reobtém após expirar]
    
    style C fill:#d4edda
    style D fill:#f8d7da
    style E fill:#fff3cd
Padrão Sintaxe Comportamento Casos de Uso
force-cache (padrão) fetch(url) ou fetch(url, { cache: 'force-cache' }) Obtido apenas durante o build ou na primeira requisição; resultados são cacheados permanentemente Dados que raramente mudam (documentos, configuração estática)
no-store fetch(url, { cache: 'no-store' }) Obtém dados novamente a cada requisição; sem cache Dados em tempo real (informações do usuário, inventário)
revalidate:N fetch(url, { next: { revalidate: 60 } }) Cache por 60 segundos; o processo em segundo plano dispara uma atualização ao expirar Dados semi-tempo real (notícias, rankings)

(1) Comportamento Padrão force-cache

Se nenhuma opção for passada, o Next.js automaticamente armazena em cache os resultados do fetch — requisições com a mesma URL e opções são feitas apenas uma vez durante o processo de build.

TSX
// app/products/page.tsx — force-cache Padrão
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json())
  // Obtém uma vez durante o build, Usa o cache depois
  return <ProductList data={products} />
}

(2) no-store Dados Dinâmicos

TSX
// app/profile/page.tsx — Obtém os dados mais recentes a cada requisição
export default async function ProfilePage() {
  const user = await fetch('https://api.example.com/me', {
    cache: 'no-store',
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
  }).then(r => r.json())
  return <ProfileView user={user} />
}

(3) revalidate Janela de Tempo

TSX
// app/blog/[slug]/page.tsx — Cache Estilo ISR
export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 }  // Usa o cache por 1 hora
  }).then(r => r.json())
  return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}

▶ Exemplo: Comparação dos Três Modos de Cache (Dificuldade: ⭐)

Saída:

TEXT 📖 Somente leitura
Obtém dados e renderiza o resultado.
TSX
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
  const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'force-cache'
  }).then(r => r.json())

  const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
    cache: 'no-store'
  }).then(r => r.json())

  return (
    <div>
      <p>Estático (force-cache): {staticData.datetime}</p>
      <p>Ao Vivo (no-store): {liveData.datetime}</p>
    </div>
  )
}

Saída:

TEXT 📖 Somente leitura
Estático (force-cache): 2026-07-06T10:00:00.000Z  ← Sempre o Mesmo
Ao Vivo (no-store): 2026-07-06T10:00:05.123Z       ← Muda a cada atualização

Saída:

TEXT 📖 Somente leitura
O navegador renderiza dois timestamps:
  Estático (force-cache): 2026-07-06T10:00:00.000Z  ← Sempre o mesmo (cacheado no build)
  Ao Vivo (no-store): 2026-07-06T10:00:05.123Z       ← Muda a cada atualização


4. Revalidação Sob Demanda: tags e revalidateTag

next: { tags: [...] } Etiqueta a requisição fetch, depois use revalidateTag(tag) para atualizar o cache conforme necessário na Server Action ou Route Handler.

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

    A->>DB: Escrever Novos Dados (Criar uma Tarefa)
    A->>Cache: revalidateTag('tasks')
    Cache->>Cache: Limpar todos os caches que correspondem à tag
    Note over Cache: Na próxima vez, o fetch Obtém Novamente
API Finalidade Onde Chamar
next: { tags: ['tasks', 'projects'] } Etiquetar o "fetch" Opções do fetch()
revalidateTag('tasks') Limpar todo cache relacionado por tag Server Action / Route Handler
revalidatePath('/dashboard') Limpar cache por caminho Server Action / Route Handler

▶ Exemplo: Usando tags e revalidateTag (Dificuldade: ⭐⭐)

TSX
// app/tasks/data.ts — Funções de Obtenção de Dados
export async function getTasks() {
  return fetch('https://api.example.com/tasks', {
    next: { tags: ['tasks'] }
  }).then(r => r.json())
}
TSX
// app/tasks/actions.ts — Server Action Atualiza o cache após escrever
'use server'
import { revalidateTag } from 'next/cache'

export async function createTask(formData: FormData) {
  const title = formData.get('title') as string
  await fetch('https://api.example.com/tasks', {
    method: 'POST',
    body: JSON.stringify({ title, status: 'todo' })
  })
  revalidateTag('tasks')  // Limpa todas as entradas em cache para esta tag
}

Saída:

TEXT 📖 Somente leitura
Obtém dados e renderiza o resultado.

▶ Exemplo: Usando revalidatePath para limpar a página inteira (Dificuldade: ⭐⭐)

TSX
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function publishArticle() {
  await db.article.update({ where: { id: 1 }, data: { published: true } })
  revalidatePath('/blog')       // Atualiza a página /blog
  revalidatePath('/blog/[slug]') // Atualiza todos os detalhes dos artigos
}

Saída:

TEXT 📖 Somente leitura
Renderiza a interface do componente publishArticle.


5. Busca Paralela de Dados e Como Evitar o Efeito Cascata (Waterfall)

O padrão cascata (waterfall) é o principal assassino de performance — cada await espera em sequência pelo anterior terminar. Usar Promise.all() permite que todas as requisições sejam iniciadas simultaneamente.

100%
graph LR
    subgraph "Padrão Cascata (Lento)"
        A1[fetch A] --> A2[fetch B] --> A3[fetch C]
        A1 -.- t1[2s]
        A2 -.- t2[+2s = 4s]
        A3 -.- t3[+2s = 6s]
    end
    subgraph "Paralelo (Rápido)"
        B1[fetch A] -.- u1[2s]
        C1[fetch B] -.- u2[2s]
        D1[fetch C] -.- u3[2s]
        B1 & C1 & D1 --> M[Promise.all<br/>Tempo Total ~2s]
    end
Padrão Implementação Tempo Total (2s cada) Cenários Aplicáveis
Cascata Serial await A; await B; await C ~6s Requisições dependentes
Requisições Paralelas Promise.all([A, B, C]) ~2s Requisições independentes
Paralelo em fases const a = await A; const [b, c] = await Promise.all([B(a.id), C]) ~4s Requisições parcialmente dependentes

▶ Exemplo: Reconhecendo o Padrão Cascata Serial (Dificuldade: ⭐)

TSX
// app/waterfall/page.tsx — ❌ Cascata Serial
export default async function WaterfallPage() {
  const user = await fetch('https://api.example.com/user').then(r => r.json())          // 1s
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())  // Espera terminar + 2s = 3s
  const details = await Promise.all(tasks.map(t =>
    fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json())  // Espera terminar + 2s = 5s
  ))

  return <div>Total: ~5s</div>
}

Saída:

TEXT 📖 Somente leitura
Obtém dados e renderiza uma lista de itens.
Texto visível: Total: ~5s

▶ Exemplo: Otimização Paralela (Dificuldade: ⭐⭐)

Saída:

TEXT 📖 Somente leitura
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
TSX
// app/no-waterfall/page.tsx — ✅ Otimização Paralela
export default async function NoWaterfallPage() {
  // Estágio 1: Obtém Usuários e Dados Iniciais Concorrentemente
  const [user, initialData] = await Promise.all([
    fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
    fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
  ])

  // Estágio 2: Requisição dependente de user.id (Ainda há pequenas cascatas, mas já é o melhor)
  const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())

  return <div>Total: ~2s (1s + 1s paralelo, depois 1s)</div>
}

Saída:

TEXT 📖 Somente leitura
Obtém dados e renderiza o resultado.
Texto visível: Total: ~2s (1s + 1s paralelo, depois 1s)

▶ Exemplo: Carregamento Lazy com Suspense (Dificuldade: ⭐⭐⭐)

Saída:

TEXT 📖 Somente leitura
A página renderiza conforme descrito acima, com a UI atualizando de acordo com o comportamento descrito.
TSX
// app/suspense-demo/page.tsx — Cada área separada usa Suspense
import { Suspense } from 'react'

export default function SuspenseDemoPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Carregando perfil...</div>}>
        <ProfileSection />
      </Suspense>
      <Suspense fallback={<div>Carregando tarefas...</div>}>
        <TaskSection />
      </Suspense>
    </div>
  )
}

async function ProfileSection() {
  const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
  return <div>Bem-vindo, {user.name}</div>
}

async function TaskSection() {
  const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
  return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}

Saída:

TEXT 📖 Somente leitura
Renderiza um shell estático imediatamente, com conteúdo dinâmico carregando dentro dos limites do Suspense.
Fallback: Carregando perfil...
Texto visível: Dashboard | Carregando perfil... | }> | Carregando tarefas...


6. Exemplo Completo: Dashboard Otimizado

TSX
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'

// ======== Funções de Dados ========
const API = 'https://jsonplaceholder.typicode.com'

async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
  const res = await fetch(`${API}${endpoint}`, {
    ...options,
    next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
  })
  if (!res.ok) throw new Error(`Falha ao buscar ${endpoint}`)
  return res.json()
}

// ======== Obtém todos os dados em paralelo ========
export default function DashboardOptimizedPage() {
  return (
    <div>
      <h1>Dashboard Otimizado</h1>
      <div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
        <Suspense fallback={<Skeleton label="Usuários" />}>
          <DataCard title="Usuários" endpoint="/users" />
        </Suspense>
        <Suspense fallback={<Skeleton label="Posts" />}>
          <DataCard title="Posts" endpoint="/posts" revalidate={120} />
        </Suspense>
        <Suspense fallback={<Skeleton label="Comentários" />}>
          <DataCard title="Comentários" endpoint="/comments" />
        </Suspense>
        <Suspense fallback={<Skeleton label="Tarefas" />}>
          <DataCard title="Tarefas" endpoint="/todos" revalidate={30} />
        </Suspense>
      </div>
    </div>
  )
}

async function DataCard({ title, endpoint, revalidate }: {
  title: string
  endpoint: string
  revalidate?: number
}) {
  const data = await getData<any[]>(endpoint, revalidate
    ? { next: { revalidate } }
    : { cache: 'no-store' }
  )
  return (
    <div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
      <h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
      <ul>{data.slice(0, 5).map((item: any) => (
        <li key={item.id}>{item.title ?? item.name ?? item.email}</li>
      ))}</ul>
    </div>
  )
}

function Skeleton({ label }: { label: string }) {
  return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
    Carregando {label}...
  </div>
}

// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function refreshSection(tag: string) {
  revalidateTag(tag)
  return { success: true }
}

❓ Perguntas Frequentes

P: As opções 'force-cache' e 'no-store' no fetch se comportam da mesma forma no modo de desenvolvimento e no modo de produção? R: Não, não se comportam. No modo de desenvolvimento (npm run dev), o force-cache ainda é buscado a cada requisição (para facilitar a depuração). O cache só entra em vigor no modo de produção (next start ou após o build). Esta é uma decisão de design do Next.js — sempre buscar os dados mais recentes durante a fase de desenvolvimento.

P: Qual é a diferença entre revalidateTag e revalidatePath? R: revalidateTag limpa o cache por tag (para os mesmos dados em páginas diferentes), enquanto revalidatePath limpa o cache por caminho (até o nível da página ou padrão de rota). O primeiro é adequado para controle refinado na camada de dados, enquanto o último é adequado para atualizações no nível da página. Recomendamos usar revalidateTag sempre que possível.

P: Se duas requisições fetch usarem a mesma URL mas opções diferentes, elas compartilharão o cache? R: Não. A chave de cache é calculada com base na URL, método, headers e body. Requisições com a mesma URL mas configurações diferentes de cache ou next.revalidate serão tratadas como entradas de cache diferentes.

P: Como o Promise.all lida com uma requisição que falha? R: Promise.all é uma operação "tudo ou nada" — se qualquer requisição falhar, toda a promise é rejeitada. Se você precisar tratar erros individualmente, envolva cada chamada fetch em Promise.allSettled ou um bloco try-catch. Um padrão comum é const results = await Promise.all(urls.map(u => fetch(u).catch(() => null))).

P: Como o timeout do fetch é tratado no RSC? R: O fetch não tem um timeout embutido. Você pode envolvê-lo em um AbortController: const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal }). Recomenda-se encapsular um cliente fetch unificado no nível da aplicação.

P: Posso usar um cliente HTTP de terceiros (como axios) no RSC? R: Sim, mas você perderá os recursos estendidos do fetch do Next.js, como cache automático, tags e revalidação. Se usar axios, você precisará implementar a lógica de cache manualmente ou envolver uma camada compatível com fetch em torno do axios. Recomendamos usar o método fetch nativo sempre que possível.


📖 Resumo


📝 Exercícios

  1. Exercício Básico (⭐): Crie um app/time-demo/page.tsx, use cache: 'no-store' e cache: 'force-cache' para fazer requisições separadas à API World Time, compare a diferença entre os dois timestamps e verifique o comportamento de cache.

  2. Exercício Avançado (⭐⭐): Construa um app/parallel-demo/page.tsx que use Promise.all para buscar /users, /posts e /comments (usando a API JSONPlaceholder), e renderize cada conjunto de dados dentro de um limite <Suspense> separado para demonstrar o efeito de carregamento em streaming.

  3. Desafio (⭐⭐⭐): Crie uma página de lista de tarefas com suporte a operações CRUD: app/tasks/page.tsx (exibe a lista de tarefas, usando tags para cache) e app/tasks/actions.ts (chama revalidateTag('tasks') para atualizar a lista após adicionar ou excluir uma tarefa). Implemente uma atualização otimista para garantir que a lista seja atualizada imediatamente após uma operação de escrita.

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%