<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>'use server'</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>'use server'</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
'use server';
import { db } from '@/lib/db';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
const email = formData.get('email') as string;
// Validação básica
if (!name || !email) {
throw new Error('Nome e email são obrigatórios');
}
// 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
'use client';
import { createUser } from '@/app/actions/user';
export default function UserForm() {
async function handleSubmit(formData: FormData) {
try {
const result = await createUser(formData);
console.log('Usuário criado:', result);
} catch (error) {
console.error('Erro:', error);
}
}
return (
<form action={handleSubmit}>
<input type="text" name="name" placeholder="Nome" required />
<input type="email" name="email" placeholder="Email" required />
<button type="submit">Criar Usuário</button>
</form>
);
}</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">'use client';
import { useTransition } from 'react';
import { createUser } from '@/app/actions/user';
export default function UserForm() {
const [isPending, startTransition] = useTransition();
function handleSubmit(formData: FormData) {
startTransition(async () => {
try {
const result = await createUser(formData);
alert(Usuário ${result.userId} criado com sucesso!);
} catch (error) {
alert('Erro ao criar usuário');
}
});
}
return (
<form action={handleSubmit}>
<input type="text" name="name" placeholder="Nome" disabled={isPending} />
<input type="email" name="email" placeholder="Email" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Criando...' : 'Criar Usuário'}
</button>
</form>
);
}</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 '@/app/lib/api';
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 <div>{user.name}</div>;
}</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 'react';
// Esta função é cacheada por requisição e revalidada a cada 1 hora
export const getProduct = cache(async (id: string) => {
const res = await fetch(https://api.shop.com/products/${id}, {
next: { revalidate: 3600 }
});
return res.json();
});
// app/products/[id]/page.tsx
import { getProduct } from '@/app/lib/database';
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
</div>
);
}
// Exports estáticos para otimizar o build
export async function generateStaticParams() {
const products = await fetch('https://api.shop.com/products').then(r => r.json());
return products.map((p) => ({ 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 '@/lib/posts';
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 (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<time>{post.publishedAt}</time>
</article>
);
}
export async function generateStaticParams() {
// Gerar parâmetros para posts populares no build
const posts = await fetch('https://api.blog.com/posts?featured=true').then(r => r.json());
return posts.map((post) => ({ 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
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
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('/blog');
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: ['post', post-${slug}] }
});
return res.json();
}
// app/actions/blog.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function updatePost(slug: string, data: any) {
const res = await fetch(https://api.blog.com/posts/${slug}, {
method: 'PUT',
body: JSON.stringify(data)
});
// Revalidar apenas posts com esta tag
revalidateTag(post-${slug});
revalidateTag('post'); // 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
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function addComment(postId: string, content: string) {
// Validar e inserir
if (!content.trim()) {
throw new Error('Comentário não pode estar vazio');
}
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
'use client';
import { addComment } from '@/app/actions/comments';
import { useTransition } from 'react';
export default function CommentSection({ postId }: { postId: string }) {
const [isPending, startTransition] = useTransition();
async function handleSubmit(formData: FormData) {
const content = formData.get('content') as string;
startTransition(async () => {
try {
await addComment(postId, content);
// Após revalidação, a página será atualizada automaticamente
} catch (error) {
console.error('Erro ao adicionar comentário:', error);
}
});
}
return (
<form action={handleSubmit}>
<textarea name="content" placeholder="Seu comentário..." disabled={isPending} />
<button disabled={isPending}>{isPending ? 'Enviando...' : 'Comentar'}</button>
</form>
);
}</code></pre>
<p>Quando o usuário clica em "Comentar", 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">'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } });
// Revalidar em paralelo
await Promise.all([
revalidateTag('posts-list'),
revalidateTag(post-${postId}),
revalidatePath('/posts'),
revalidatePath('/dashboard')
]);
}</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(
'https://api.shop.com/products?sort=sales&limit=100'
).then(r => r.json());
return products.map(p => ({ 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 <div>{product.name}</div>;
}</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><!-- FIM --></p>