React Server Components na prática: o que mudou no modelo mental do dev
1. Desconstruindo o paradigma: do CSR ao RSC
1.1. O que são React Server Components (RSC) e por que eles quebram a hegemonia do Client-Side Rendering
React Server Components representam uma mudança fundamental na arquitetura do React. Antes dos RSC, todo componente React era renderizado no cliente — o servidor enviava um bundle JavaScript, e o navegador executava tudo. Com RSC, componentes podem ser renderizados exclusivamente no servidor, gerando HTML que é enviado ao cliente sem necessidade de JavaScript para renderização inicial.
Isso quebra a hegemonia do Client-Side Rendering porque agora temos componentes que nunca chegam ao bundle do cliente. O impacto é imediato: redução drástica no tamanho do JavaScript, melhoria na performance de carregamento e melhor SEO.
1.2. Diferenças fundamentais: Server Components vs Client Components
A diferença essencial está em onde o componente é executado:
// Server Component (padrão no Next.js App Router)
// Executa no servidor, nunca no cliente
async function PostList() {
const posts = await fetch('https://api.exemplo.com/posts').then(r => r.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Client Component (exige diretiva 'use client')
'use client';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Server Components não podem usar hooks de estado (useState, useEffect), eventos do navegador (onClick) ou APIs do cliente (localStorage). Client Components podem usar tudo isso, mas aumentam o bundle.
1.3. O mito do “tudo no servidor”
O equilíbrio ideal não é “tudo no servidor” ou “tudo no cliente”. A regra prática:
- Server Components: busca de dados, conteúdo estático, componentes que não precisam de interatividade
- Client Components: formulários, animações, componentes com estado, listeners de eventos
Um erro comum é tentar colocar lógica interativa em Server Components — isso simplesmente não funciona.
2. A nova divisão de responsabilidades no componente
2.1. O fim do useEffect para busca de dados
Antes dos RSC, toda busca de dados exigia useEffect + estado de loading:
// Abordagem antiga (CSR)
'use client';
import { useState, useEffect } from 'react';
function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(r => r.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <p>Carregando...</p>;
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Com RSC, a busca é direta e síncrona (do ponto de vista do componente):
// Abordagem RSC
async function Posts() {
const posts = await fetch('https://api.exemplo.com/posts').then(r => r.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Sem estado de loading, sem useEffect, sem hydration desnecessário.
2.2. Server Actions: substituindo APIs REST/GraphQL para mutações
Server Actions permitem que funções definidas no servidor sejam chamadas diretamente do cliente:
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
// Validação e persistência no servidor
await db.post.create({ data: { title, content } });
// Revalidação automática do cache
revalidatePath('/posts');
}
// app/components/PostForm.tsx
'use client';
import { createPost } from '@/app/actions';
function PostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Criar Post</button>
</form>
);
}
Isso elimina a necessidade de criar endpoints REST ou mutations GraphQL para operações CRUD simples.
2.3. A linha tênue entre estado do servidor e estado do cliente
O estado agora tem duas camadas:
- Estado do servidor: dados buscados via fetch, cache automático, revalidação
- Estado do cliente: interatividade pura (contadores, modais, formulários não submetidos)
A regra: dados que vêm do servidor devem ser gerenciados pelo servidor. Não duplique estado do servidor no cliente.
3. Modelo mental: “pensar em fronteiras” entre servidor e cliente
3.1. A regra do “use client”: o que força um componente a ser do lado do cliente
A diretiva 'use client' cria uma fronteira. Qualquer componente que:
- Use hooks (useState, useEffect, useContext)
- Responda a eventos do usuário (onClick, onSubmit)
- Acesse APIs do navegador (localStorage, window)
...deve ser um Client Component.
// Isso NÃO funciona em Server Component
function SearchInput() {
const [query, setQuery] = useState(''); // ERRO: useState não disponível
return <input onChange={(e) => setQuery(e.target.value)} />;
}
3.2. Serialização de props: o que pode (e o que NÃO pode) passar do servidor pro cliente
Props passadas de Server para Client Components precisam ser serializáveis:
✅ Pode passar: strings, números, booleanos, objetos simples, arrays, Date (serializado como string)
❌ Não pode passar: funções, classes, React elementos, símbolos, Promises não resolvidas
// Server Component passando props para Client Component
async function Page() {
const posts = await getPosts();
return (
<ClientList
items={posts} // ✅ Objeto serializável
onSelect={(id) => console.log(id)} // ❌ Função não serializável
/>
);
}
3.3. Composição híbrida: aninhando Server Components dentro de Client Components
A técnica de composição permite manter Server Components dentro de Client Components:
// Client Component
'use client';
function ClientLayout({ children }: { children: React.ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
{sidebarOpen && children} {/* children pode ser um Server Component */}
</div>
);
}
// Server Component usando ClientLayout
function Page() {
return (
<ClientLayout>
<ServerSidebar /> {/* Server Component passado como children */}
</ClientLayout>
);
}
4. Performance na prática: streaming, suspense e carregamento progressivo
4.1. Streaming de Server Components
RSC permitem streaming — o React envia HTML em chunks conforme os dados ficam prontos:
async function SlowComponent() {
await new Promise(resolve => setTimeout(resolve, 3000));
return <p>Dados carregados após 3 segundos</p>;
}
async function FastComponent() {
return <p>Renderizado imediatamente</p>;
}
function Page() {
return (
<div>
<FastComponent /> {/* Renderizado primeiro */}
<Suspense fallback={<p>Carregando...</p>}>
<SlowComponent /> {/* Streamado depois */}
</Suspense>
</div>
);
}
4.2. Suspense Boundaries no servidor
Suspense funciona no servidor com RSC. O fallback é renderizado enquanto o componente assíncrono é resolvido:
function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<UserData />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<ChartData />
</Suspense>
</div>
);
}
4.3. Cache e revalidação
O fetch nativo do Next.js tem cache automático:
// Cache automático (dados estáticos)
const data = await fetch('https://api.exemplo.com/posts');
// Sem cache (dados dinâmicos)
const data = await fetch('https://api.exemplo.com/posts', { cache: 'no-store' });
// Revalidação por tempo
const data = await fetch('https://api.exemplo.com/posts', { next: { revalidate: 60 } });
5. Implicações no ecossistema e nas bibliotecas
5.1. Impacto em bibliotecas de estado e data fetching
- Redux/Zustand: perdem relevância para estado do servidor. Dados devem vir dos RSC, não do store
- React Query/SWR: ainda úteis para cache no cliente e mutações, mas parte da busca migra para RSC
- O novo padrão:
fetchdireto no servidor + Server Actions para mutações
5.2. Adaptação de UI libraries
- Shadcn: já adaptado — componentes são Client Components por padrão
- MUI: requer
'use client'em componentes interativos - Radix UI: funciona bem, mas requer
'use client'em componentes com estado
5.3. Testes e debugging
Testar RSC exige ambiente de servidor:
// Testando Server Component
import { renderToString } from 'react-dom/server';
const html = await renderToString(<PostList />);
expect(html).toContain('Título do Post');
Para debugging, use logs no servidor (console.log aparece no terminal, não no navegador).
6. Casos reais de migração
6.1. Migrando página de listagem de posts
Antes (CSR):
'use client';
function PostsPage() {
const [posts, setPosts] = useState([]);
useEffect(() => { fetch('/api/posts').then(r => r.json()).then(setPosts); }, []);
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Depois (RSC):
async function PostsPage() {
const posts = await fetch('https://api.exemplo.com/posts').then(r => r.json());
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
6.2. Formulário com Server Action
// Server Action
'use server';
export async function addTodo(formData: FormData) {
const title = formData.get('title');
await db.todo.create({ data: { title, completed: false } });
revalidatePath('/todos');
}
// Client Component
'use client';
function TodoForm() {
return (
<form action={addTodo}>
<input name="title" required />
<button type="submit">Adicionar</button>
</form>
);
}
6.3. Armadilhas comuns
- Importar biblioteca Node.js no cliente:
fs,cryptonão funcionam em Client Components - Vazamento de lógica de servidor: senhas, tokens de API não devem ser passados como props
'use client'em componente pai: força toda a árvore abaixo a ser cliente
7. O novo modelo mental do desenvolvedor React
7.1. De “pensar em componentes” para “pensar em ambientes”
O desenvolvedor agora precisa decidir: este componente roda onde? A resposta determina o que pode ser usado.
7.2. A mudança na hierarquia de preocupações
- Dados: buscar no servidor (RSC)
- Estado: gerenciar interatividade mínima (Client Components)
- Interatividade: adicionar apenas onde necessário
7.3. RSC como um passo rumo a um React “full-stack por padrão”
O React está evoluindo para um framework full-stack. RSC são a base para:
- Menos JavaScript no cliente
- Melhor performance percebida
- Código mais próximo do banco de dados
- Menos boilerplate (APIs, hooks de fetch, estados de loading)
O modelo mental mudou: pense em onde o código executa, não apenas no que ele faz.
Referências
- React Server Components: Documentação Oficial — Guia completo sobre Server Components, diretivas e boas práticas
- Next.js: Server Components e Client Components — Documentação do Next.js sobre a implementação prática de RSC
- Server Actions no Next.js — Tutorial oficial sobre mutações com Server Actions
- Padrões de Composição com RSC — Artigo técnico sobre padrões de composição entre Server e Client Components
- Streaming e Suspense no React 18 — Blog da Vercel explicando streaming e carregamento progressivo com RSC
- Migrando para RSC: Guia Prático — Tutorial passo a passo de migração de CSR para RSC com exemplos reais
- Testing Server Components — Vídeo técnico sobre estratégias de teste para componentes que rodam no servidor