Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · Next.js e Meta-frameworks · 0/10
Recomendado: essencial

Data Fetching no Server: cache, revalidação, Suspense

5 min de leitura

fonte

Em SPA, fetch é "faço no useEffect, guardo no state, rezo pra não ter loading infinito". Em Next com App Router, fetch é declaração: você diz "busca isso, cacheia por X segundos, marca com essa tag". O framework se vira. Este nó é sobre como controlar isso sem ficar perdido.

O essencial 🟢

Em Server Component, await fetch(...) funciona igual a código de backend. Você chama a função, dá await, usa o resultado. Sem useEffect, sem useState, sem loading state.

// app/posts/page.tsx (Server)
export default async function PostsPage() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
  return (
    <ul>
      {posts.map((p: { id: string; title: string }) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

A primeira vista, parece o mesmo fetch que você já conhece. A diferença: o Next intercepta o fetch e adiciona cache.

O Next cacheia automaticamente o resultado de fetch entre requests (Data Cache). Por padrão, fetch em Server Component é cacheado indefinidamente - a mesma URL é chamada uma vez, o resultado é guardado, e requests subsequentes usam o cache.

// Cacheia pra sempre (default em Server Components, Next 15).
const res = await fetch("https://api.example.com/posts");

// Cacheia por 60 segundos e revalida depois.
const res = await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 },
});

// Não cacheia (sempre busca de novo).
const res = await fetch("https://api.example.com/posts", {
  cache: "no-store",
});

fetch(URL, { next: { revalidate: 60 } }) é ISR-like. ISR (Incremental Static Regeneration) era o nome do Pages Router pra "revalida a cada X segundos". No App Router, você faz a mesma coisa com revalidate. O Next guarda o cache, serve o cache velho, e em background busca de novo. Usuário nunca espera.

fetch(URL, { next: { tags: ['posts'] } }) é o que destrava mutations. Você marca o cache com uma tag. Depois, em qualquer lugar (Server Action, Route Handler), você chama revalidateTag('posts') e todo fetch com aquela tag é invalidado.

// app/posts/page.tsx
const res = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

// app/posts/new/actions.ts (Server Action)
"use server";
import { revalidateTag } from "next/cache";

export async function createPost(formData: FormData) {
  await db.post.create({ data: { ... } });
  revalidateTag("posts"); // invalida o cache marcado com "posts"
  // Próxima renderização de PostsPage vai buscar de novo.
}

revalidatePath('/posts') invalida a rota inteira. Mais agressivo que revalidateTag: invalida tudo associado àquela rota, independente de tag. Use quando você sabe que toda a rota muda (ex: mudança de layout, não de dado específico).

import { revalidatePath, revalidateTag } from "next/cache";

// Invalida só os fetches com tag "posts"
revalidateTag("posts");

// Invalida toda a rota /posts (incluindo layout, page, fetches)
revalidatePath("/posts");

// Invalida uma rota dinâmica
revalidatePath(`/posts/${slug}`);

noStore() (e connection()) para dados por usuário. Por padrão, o Next tenta cachear tudo. Mas tem dados que são específicos do usuário (carrinho, dashboard pessoal) e não podem ser compartilhados. Pra desabilitar cache de toda a rota, use noStore() no server:

// app/dashboard/page.tsx
import { unstable_noStore as noStore } from "next/cache";

export default async function DashboardPage() {
  noStore(); // marca a rota como dinâmica
  const userData = await fetchUserData(); // nunca cacheado
  return <div>...</div>;
}

noStore é "force-dynamic" granular. A rota inteira deixa de ser estática e vira SSR por demanda.

Acessar cookies() ou headers() também força dynamic. O Next detecta que você depende de request-specific data e desliga o cache automaticamente:

import { cookies } from "next/headers";

export default async function Page() {
  const cookieStore = await cookies(); // Next 15+: Promise
  const session = cookieStore.get("session");
  // Já marca a rota como dinâmica, sem precisar de noStore().
  return <div>...</div>;
}

Aprofundamento 🟡

Os 4 caches do Next (mapa completo). O Next tem 4 camadas de cache, e confundi-las é a maior fonte de bugs de "por que minha mudança não apareceu?".

Os 4 caches do Next: Router Cache (client), Full Route Cache (HTML+RSC), Data Cache (fetch), Request Memoization (intra-request). Invalidação flui de baixo pra cima.
  1. Request Memoization (intra-request): mesma URL chamada várias vezes na mesma renderização retorna o mesmo objeto (sem refetch). Automático, sem config.

  2. Data Cache (cross-request, server-side): o resultado de fetch é cacheado por padrão. revalidate ou tags controlam expiração e invalidação. Server-only.

  3. Full Route Cache (cross-request, server-side): o HTML e RSC payload renderizados são cacheados. Static rendering = build time. Dynamic rendering = on demand.

  4. Router Cache (client-side, in-memory): o RSC payload de rotas já visitadas fica na memória do client. Sobrevive a navegação, perde em refresh. É o que faz a navegação "instantânea".

Por que fetch foi escolhido (e não XHR, axios, etc.). O Next só cacheia fetch nativo do Node/browser. Se você usar axios, got, node-fetch (versões antigas), prisma.findMany direto, etc., o Next não tem como cachear (não vê a chamada). Por isso a convenção é: use fetch pra HTTP, e use libs que retornam o resultado via fetch internamente ou expõem cache próprio.

cache: 'force-cache' vs default (mesma coisa em Next 15). O default do fetch em Server Component é cachear. force-cache é explícito. Ambos se comportam igual. Use default por padrão, force-cache quando quiser deixar a intenção clara.

Quando NÃO confiar no cache do Next: dados por usuário. Já mencionado: cookies, headers, searchParams específicos do user desligam o cache automaticamente. Se você tá vendo "dados do outro usuário" no seu dashboard, é porque você não tá lendo cookies/headers - adicione await cookies() (mesmo sem usar o valor) pra forçar dynamic.

import { cookies } from "next/headers";

export default async function DashboardPage() {
  // Lendo cookie (mesmo que sem usar) força a rota a ser dinâmica.
  await cookies();
  const data = await fetchDashboardData();
  return <div>{data.name}</div>;
}

React.cache() pra memoizar função pura entre requests no mesmo render. Útil quando você tem uma função que faz query reutilizada por múltiplos componentes:

// lib/posts.ts
import { cache } from "react";
import { db } from "./db";

export const getPost = cache(async (slug: string) => {
  return db.post.findUnique({ where: { slug } });
});

// app/posts/[slug]/page.tsx
const post = await getPost(slug); // 1ª chamada - query real

// app/posts/[slug]/Sidebar.tsx (também server)
const samePost = await getPost(slug); // mesma chamada - cacheada no request

Sem cache(), os dois componentes fariam 2 queries ao banco. Com cache(), fazem 1. (Request Memoization faz a mesma coisa pro fetch, mas não pra query direta de banco.)

generateStaticParams + revalidate = o "melhor dos dois mundos". Pré-renderiza as rotas mais acessadas em build time, revalida em background:

// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await db.post.findMany({ where: { published: true } });
  return posts.map((p) => ({ slug: p.slug }));
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug);
  return <article>{post.content}</article>;
}

Rotas de generateStaticParams ficam em SSG (build time). A rota inteira (Full Route Cache) é estática. revalidateTag na mutation invalida quando você cria/edita/deleta.

dynamicParams controla slugs fora do generateStaticParams. Default é true (slugs não-pré-renderizados são SSR por demanda). false faz eles retornarem 404.

Pra quem quer ir além 🔴

On-demand revalidation via webhook. Plataformas tipo Contentful, Sanity, ou seu próprio backend mandam um webhook quando conteúdo muda. O handler chama revalidateTag:

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";

export async function POST(req: Request) {
  const { tag, secret } = await req.json();
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: "unauthorized" }, { status: 401 });
  }
  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

O conteúdo do CMS atualiza → manda POST → Next invalida cache → próxima renderização busca o novo conteúdo.

Time-based revalidation: como o ISR evoluiu. No Pages Router, revalidate: 60 revalidava a página inteira. No App Router, revalidate no fetch revalida só aquele fetch. Combinar revalidate em vários fetches dá controle fino.

Streaming + data fetching: cada <Suspense> libera quando sua data chega. O Next renderiza o HTML em chunks. Cada <Suspense> libera quando o fetch dentro dele resolve. O browser recebe e renderiza progressivamente. Veremos a fundo no nó streaming-ssr.

Leitura recomendada:

Dica: a primeira vez que você vê "minha mudança não apareceu", 9 em 10 vezes é cache. Antes de debugar, faça revalidateTag da tag certa, ou revalidatePath da rota, e veja se resolve. Cache do Next é agressivo por design (performance), e por isso é fonte de bugs pra quem vem de SPA.

No próximo nó, vamos ver Server Actions - o caminho moderno pra mutations: revalidateTag pós-create/update/delete, validação server-side, e progressive enhancement (form funciona sem JS).

// Quiz

Qual a diferença entre `revalidateTag('posts')` e `revalidatePath('/posts')`?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações