React & Frontend

O que Todo Dev Deve Saber sobre Next.js App Router Avançado: Server Actions, Cache e Revalidação

16 min de leitura

O que Todo Dev Deve Saber sobre Next.js App Router Avançado: Server Actions, Cache e Revalidação

Server Actions: Fundamentos e Implementação Server Actions são funções executadas no servidor que podem ser chamadas diretamente do cliente sem necessidade de criar rotas API explícitas. Elas representam um paradigma diferente de comunicação cliente-servidor: em vez de fazer requisições HTTP para endpoints, você executa código do servidor de forma declarativa e type-safe usando TypeScript. A verdadeira potência das Server Actions está na sua simplicidade e segurança. Quando você marca uma função com , o Next.js automaticamente cria um endpoint seguro para essa função, valida os tipos de dados, serializa argumentos e respostas, e previne exposição de código sensível. Você não precisa gerenciar autenticação, validação de requisição ou tratamento de CORS — tudo é abstraído pela framework. Criando sua primeira Server Action Para criar uma Server Action, defina uma função assíncrona com a diretiva no topo do arquivo. Esta função pode ser criada em um arquivo separado ou no próprio componente: Agora você pode chamar essa função diretamente de um componente

<h2>Server Actions: Fundamentos e Implementação</h2>

<p>Server Actions são funções executadas no servidor que podem ser chamadas diretamente do cliente sem necessidade de criar rotas API explícitas. Elas representam um paradigma diferente de comunicação cliente-servidor: em vez de fazer requisições HTTP para endpoints, você executa código do servidor de forma declarativa e type-safe usando TypeScript.</p>

<p>A verdadeira potência das Server Actions está na sua simplicidade e segurança. Quando você marca uma função com <code>&#039;use server&#039;</code>, o Next.js automaticamente cria um endpoint seguro para essa função, valida os tipos de dados, serializa argumentos e respostas, e previne exposição de código sensível. Você não precisa gerenciar autenticação, validação de requisição ou tratamento de CORS — tudo é abstraído pela framework.</p>

<h3>Criando sua primeira Server Action</h3>

<p>Para criar uma Server Action, defina uma função assíncrona com a diretiva <code>&#039;use server&#039;</code> no topo do arquivo. Esta função pode ser criada em um arquivo separado ou no próprio componente:</p>

<pre><code class="language-typescript">// app/actions/user.ts

&#039;use server&#039;;

import { db } from &#039;@/lib/db&#039;;

export async function createUser(formData: FormData) {

const name = formData.get(&#039;name&#039;) as string;

const email = formData.get(&#039;email&#039;) as string;

// Validação básica

if (!name || !email) {

throw new Error(&#039;Nome e email são obrigatórios&#039;);

}

// Executar no servidor

const user = await db.user.create({

data: { name, email }

});

return { success: true, userId: user.id };

}</code></pre>

<p>Agora você pode chamar essa função diretamente de um componente cliente:</p>

<pre><code class="language-typescript">// app/components/UserForm.tsx

&#039;use client&#039;;

import { createUser } from &#039;@/app/actions/user&#039;;

export default function UserForm() {

async function handleSubmit(formData: FormData) {

try {

const result = await createUser(formData);

console.log(&#039;Usuário criado:&#039;, result);

} catch (error) {

console.error(&#039;Erro:&#039;, error);

}

}

return (

&lt;form action={handleSubmit}&gt;

&lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;Nome&quot; required /&gt;

&lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;Email&quot; required /&gt;

&lt;button type=&quot;submit&quot;&gt;Criar Usuário&lt;/button&gt;

&lt;/form&gt;

);

}</code></pre>

<h3>Trabalhando com useTransition para melhor UX</h3>

<p>Quando você chama uma Server Action, o estado é mantido em aberto até que a requisição seja concluída. Use o hook <code>useTransition</code> para fornecer feedback ao usuário durante a execução:</p>

<pre><code class="language-typescript">&#039;use client&#039;;

import { useTransition } from &#039;react&#039;;

import { createUser } from &#039;@/app/actions/user&#039;;

export default function UserForm() {

const [isPending, startTransition] = useTransition();

function handleSubmit(formData: FormData) {

startTransition(async () =&gt; {

try {

const result = await createUser(formData);

alert(Usuário ${result.userId} criado com sucesso!);

} catch (error) {

alert(&#039;Erro ao criar usuário&#039;);

}

});

}

return (

&lt;form action={handleSubmit}&gt;

&lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;Nome&quot; disabled={isPending} /&gt;

&lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;Email&quot; disabled={isPending} /&gt;

&lt;button type=&quot;submit&quot; disabled={isPending}&gt;

{isPending ? &#039;Criando...&#039; : &#039;Criar Usuário&#039;}

&lt;/button&gt;

&lt;/form&gt;

);

}</code></pre>

<p>O <code>isPending</code> indica quando a requisição está em andamento, permitindo desabilitar inputs, mostrar spinners ou alterar o texto do botão. Esta é a forma recomendada de gerenciar estados de carregamento em Server Actions.</p>

<h2>Cache no Next.js App Router</h2>

<p>O caching no Next.js é um dos aspectos mais sofisticados e frequentemente mal compreendidos. Existem quatro camadas distintas de cache que funcionam juntas: Request Memoization, Data Cache, Full Route Cache e Client-side Router Cache. Cada uma tem seu próprio tempo de vida e propósito.</p>

<p>O <strong>Request Memoization</strong> deduplica requisições idênticas durante uma única renderização do servidor. Se você faz duas chamadas ao banco de dados com os mesmos parâmetros na mesma requisição HTTP, apenas uma executa. O <strong>Data Cache</strong> persiste dados entre requisições no build time até que você explicitamente revalide. O <strong>Full Route Cache</strong> cacheia a renderização HTML completa. O <strong>Client-side Router Cache</strong> mantém rotas visitadas em memória no navegador.</p>

<h3>Request Memoization: Evitando duplicação</h3>

<p>React estende fetch para automaticamente memoizar requisições idênticas. Isso significa que se você chamar a mesma URL com os mesmos parâmetros múltiplas vezes durante uma renderização, apenas uma requisição de rede é feita:</p>

<pre><code class="language-typescript">// app/lib/api.ts

export async function getUser(id: string) {

const res = await fetch(https://api.example.com/users/${id}, {

next: { revalidate: 3600 } // 1 hora

});

return res.json();

}

// app/components/UserProfile.tsx

import { getUser } from &#039;@/app/lib/api&#039;;

export default async function UserProfile({ userId }: { userId: string }) {

// Ambas as chamadas resultam em apenas uma requisição

const user = await getUser(userId);

const userAgain = await getUser(userId);

return &lt;div&gt;{user.name}&lt;/div&gt;;

}</code></pre>

<p>Este mecanismo é transparente. Na prática, significa que você pode estruturar código sem se preocupar com requisições duplicadas na mesma renderização.</p>

<h3>Data Cache e revalidação com next/cache</h3>

<p>O Data Cache persiste dados entre requisições até revalidação. Por padrão, <code>fetch()</code> com <code>next.revalidate</code> cria dados cacheados. Você pode controlar o cache de forma granular:</p>

<pre><code class="language-typescript">// app/lib/database.ts

import { cache } from &#039;react&#039;;

// Esta função é cacheada por requisição e revalidada a cada 1 hora

export const getProduct = cache(async (id: string) =&gt; {

const res = await fetch(https://api.shop.com/products/${id}, {

next: { revalidate: 3600 }

});

return res.json();

});

// app/products/[id]/page.tsx

import { getProduct } from &#039;@/app/lib/database&#039;;

export default async function ProductPage({ params }: { params: { id: string } }) {

const product = await getProduct(params.id);

return (

&lt;div&gt;

&lt;h1&gt;{product.name}&lt;/h1&gt;

&lt;p&gt;{product.description}&lt;/p&gt;

&lt;span&gt;${product.price}&lt;/span&gt;

&lt;/div&gt;

);

}

// Exports estáticos para otimizar o build

export async function generateStaticParams() {

const products = await fetch(&#039;https://api.shop.com/products&#039;).then(r =&gt; r.json());

return products.map((p) =&gt; ({ id: p.id }));

}</code></pre>

<h3>Full Route Cache e Incremental Static Regeneration (ISR)</h3>

<p>O Full Route Cache combina renderização estática com revalidação agendada. Rotas estáticas são pré-renderizadas no build time e servidas do cache até a revalidação:</p>

<pre><code class="language-typescript">// app/blog/[slug]/page.tsx

import { getPost } from &#039;@/lib/posts&#039;;

export const revalidate = 60; // Revalidar a cada 60 segundos

export default async function BlogPost({ params }: { params: { slug: string } }) {

const post = await getPost(params.slug);

return (

&lt;article&gt;

&lt;h1&gt;{post.title}&lt;/h1&gt;

&lt;p&gt;{post.content}&lt;/p&gt;

&lt;time&gt;{post.publishedAt}&lt;/time&gt;

&lt;/article&gt;

);

}

export async function generateStaticParams() {

// Gerar parâmetros para posts populares no build

const posts = await fetch(&#039;https://api.blog.com/posts?featured=true&#039;).then(r =&gt; r.json());

return posts.map((post) =&gt; ({ slug: post.slug }));

}</code></pre>

<p>Quando uma requisição chega em <code>/blog/meu-artigo</code>, se o cache ainda é válido, a resposta cacheada é retornada instantaneamente. Após 60 segundos, a próxima requisição dispara uma revalidação em segundo plano.</p>

<h2>Revalidação: Atualizando dados em tempo real</h2>

<p>Revalidação é o mecanismo que permite atualizar dados cacheados sem reconstruir toda a aplicação. Existem duas estratégias: <strong>time-based revalidation</strong> (revalidação em intervalos) e <strong>on-demand revalidation</strong> (revalidação acionada por eventos).</p>

<p>A revalidação time-based é declarativa e automática — você define um intervalo e o Next.js cuida do resto. A revalidação on-demand é imperativa — você explicitamente informa ao Next.js que dados específicos mudaram e precisam ser atualizados.</p>

<h3>On-demand revalidation com revalidatePath e revalidateTag</h3>

<p><code>revalidatePath()</code> invalida o cache de uma rota específica. Use-a quando um usuário executa uma ação que afeta aquela página:</p>

<pre><code class="language-typescript">// app/actions/blog.ts

&#039;use server&#039;;

import { revalidatePath } from &#039;next/cache&#039;;

import { db } from &#039;@/lib/db&#039;;

export async function publishPost(postId: string) {

// Atualizar no banco de dados

const post = await db.post.update({

where: { id: postId },

data: { published: true }

});

// Revalidar a página do blog

revalidatePath(&#039;/blog&#039;);

revalidatePath(/blog/${post.slug});

return post;

}</code></pre>

<p><code>revalidateTag()</code> oferece controle mais granular. Você marca requisições com tags e depois invalida especificamente essas tags:</p>

<pre><code class="language-typescript">// app/lib/posts.ts

export async function getPost(slug: string) {

const res = await fetch(https://api.blog.com/posts/${slug}, {

next: { tags: [&#039;post&#039;, post-${slug}] }

});

return res.json();

}

// app/actions/blog.ts

&#039;use server&#039;;

import { revalidateTag } from &#039;next/cache&#039;;

export async function updatePost(slug: string, data: any) {

const res = await fetch(https://api.blog.com/posts/${slug}, {

method: &#039;PUT&#039;,

body: JSON.stringify(data)

});

// Revalidar apenas posts com esta tag

revalidateTag(post-${slug});

revalidateTag(&#039;post&#039;); // Ou a tag genérica

return res.json();

}</code></pre>

<p>Esta abordagem é superior para cenários com múltiplos recursos, pois permite invalidar especificamente o que mudou sem tocar em outros caches.</p>

<h3>Revalidação em Server Actions com feedback imediato</h3>

<p>Combine Server Actions com revalidação para criar experiências reativas onde os dados são atualizados imediatamente após uma ação:</p>

<pre><code class="language-typescript">// app/actions/comments.ts

&#039;use server&#039;;

import { revalidateTag } from &#039;next/cache&#039;;

import { db } from &#039;@/lib/db&#039;;

export async function addComment(postId: string, content: string) {

// Validar e inserir

if (!content.trim()) {

throw new Error(&#039;Comentário não pode estar vazio&#039;);

}

const comment = await db.comment.create({

data: { postId, content }

});

// Revalidar o cache de comentários para este post

revalidateTag(comments-${postId});

return comment;

}

// app/lib/comments.ts

export async function getComments(postId: string) {

const res = await fetch(https://api.blog.com/comments/${postId}, {

next: { tags: [comments-${postId}] }

});

return res.json();

}

// app/components/CommentSection.tsx

&#039;use client&#039;;

import { addComment } from &#039;@/app/actions/comments&#039;;

import { useTransition } from &#039;react&#039;;

export default function CommentSection({ postId }: { postId: string }) {

const [isPending, startTransition] = useTransition();

async function handleSubmit(formData: FormData) {

const content = formData.get(&#039;content&#039;) as string;

startTransition(async () =&gt; {

try {

await addComment(postId, content);

// Após revalidação, a página será atualizada automaticamente

} catch (error) {

console.error(&#039;Erro ao adicionar comentário:&#039;, error);

}

});

}

return (

&lt;form action={handleSubmit}&gt;

&lt;textarea name=&quot;content&quot; placeholder=&quot;Seu comentário...&quot; disabled={isPending} /&gt;

&lt;button disabled={isPending}&gt;{isPending ? &#039;Enviando...&#039; : &#039;Comentar&#039;}&lt;/button&gt;

&lt;/form&gt;

);

}</code></pre>

<p>Quando o usuário clica em &quot;Comentar&quot;, a Server Action executa no servidor, valida os dados, insere no banco e revalida o tag de comentários. Ao retornar, o Router Cache do cliente é atualizado automaticamente e os dados frescos são exibidos.</p>

<h2>Padrões avançados e armadilhas comuns</h2>

<p>Dominar Server Actions, Cache e Revalidação exige compreender padrões sofisticados e evitar armadilhas que degradam performance ou criam comportamentos inesperados.</p>

<h3>Erro: Confundir camadas de cache</h3>

<p>Um erro comum é assumir que revalidar com <code>revalidatePath()</code> invalida tudo. Cada camada de cache é independente. Se você não marca uma requisição com <code>next.revalidate</code> ou tags, ela não é cacheada pelo Data Cache — apenas pelo Request Memoization durante aquela renderização específica:</p>

<pre><code class="language-typescript"></code></pre>

<h3>Sincronizando múltiplas revalidações</h3>

<p>Quando uma ação afeta vários recursos, revalidate tudo simultaneamente para evitar inconsistências:</p>

<pre><code class="language-typescript">&#039;use server&#039;;

import { revalidateTag, revalidatePath } from &#039;next/cache&#039;;

export async function deletePost(postId: string) {

await db.post.delete({ where: { id: postId } });

// Revalidar em paralelo

await Promise.all([

revalidateTag(&#039;posts-list&#039;),

revalidateTag(post-${postId}),

revalidatePath(&#039;/posts&#039;),

revalidatePath(&#039;/dashboard&#039;)

]);

}</code></pre>

<h3>Limitando revalidação a rotas específicas com generateStaticParams</h3>

<p>Para aplicações com milhares de rotas dinâmicas, renderizar todas no build é inviável. Use <code>generateStaticParams</code> para renderizar apenas as importantes e deixar outras sob demanda:</p>

<pre><code class="language-typescript">// app/products/[id]/page.tsx

export const dynamicParams = true; // Permitir parâmetros não-pré-renderizados

export async function generateStaticParams() {

// Apenas renderizar os 100 produtos mais vendidos no build

const products = await fetch(

&#039;https://api.shop.com/products?sort=sales&amp;limit=100&#039;

).then(r =&gt; r.json());

return products.map(p =&gt; ({ id: p.id }));

}

export const revalidate = 3600; // Revalidar a cada hora

export default async function ProductPage({ params }: { params: { id: string } }) {

const product = await getProduct(params.id);

return &lt;div&gt;{product.name}&lt;/div&gt;;

}</code></pre>

<p>Outros produtos são renderizados sob demanda na primeira requisição e então cacheados.</p>

<h2>Conclusão</h2>

<p>Dominando Server Actions, você elimina a necessidade de criar rotas API manualmente — a framework cuida de serialização, autenticação e validação. Isso não apenas reduz código, como torna seu aplicativo mais seguro por padrão. Use <code>useTransition</code> para fornecer feedback durante operações assíncronas.</p>

<p>As quatro camadas de cache (Request Memoization, Data Cache, Full Route Cache, Client Router Cache) trabalham em conjunto para oferecer performance excepcional. Compreender qual camada você está utilizando é fundamental. Request Memoization é transparente. Data Cache requer declaração explícita com <code>next.revalidate</code> ou tags. Full Route Cache funciona apenas com rotas renderizáveis estaticamente.</p>

<p>A revalidação on-demand com <code>revalidateTag()</code> é mais poderosa que <code>revalidatePath()</code> em aplicações complexas porque oferece controle granular sobre quais dados foram afetados. Combine Server Actions com revalidação para criar experiências reativas onde dados são atualizados imediatamente após uma ação do usuário.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions" target="_blank" rel="noopener noreferrer">Next.js Official Documentation - Server Actions</a></li>

<li><a href="https://nextjs.org/docs/app/building-your-application/caching" target="_blank" rel="noopener noreferrer">Next.js Caching Documentation</a></li>

<li><a href="https://vercel.com/blog/understanding-next-js-caching" target="_blank" rel="noopener noreferrer">Vercel Blog - Understanding Next.js Caching</a></li>

<li><a href="https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration" target="_blank" rel="noopener noreferrer">Next.js Official Documentation - Incremental Static Regeneration</a></li>

<li><a href="https://www.youtube.com/watch?v=SE4Z3f4qngg" target="_blank" rel="noopener noreferrer">Lee Robinson - Next.js App Router Deep Dive (YouTube)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Hooks para Animações: useSpring, useAnimate e Física de Movimento na Prática
Hooks para Animações: useSpring, useAnimate e Física de Movimento na Prática

Entendendo Hooks para Animações no React Animações são fundamentais para cria...

Boas Práticas de React Query: Mutations, Invalidações e Sincronização de Cache para Times Ágeis
Boas Práticas de React Query: Mutations, Invalidações e Sincronização de Cache para Times Ágeis

Introdução ao React Query e o Ciclo de Vida das Mutações React Query é uma bi...

Streaming SSR com React: Suspense no Servidor e Progressive Hydration na Prática
Streaming SSR com React: Suspense no Servidor e Progressive Hydration na Prática

O Que É Streaming SSR com React? Streaming SSR (Server-Side Rendering com Str...