React Server Components no Next.js 14: O que realmente mudou
React Server Components (RSC) não são apenas “componentes que rodam no servidor”. Eles representam uma mudança no modelo mental do React — e a maioria dos artigos explica o o que sem explicar o porquê.
O problema que RSC resolve
No modelo antigo (Pages Router), você tinha duas opções:
getServerSideProps: busca dados no servidor, renderiza HTML, hidrata no cliente- Client components: busca dados no cliente, JavaScript bundle pesado
O problema: hidratação envia o mesmo dado duas vezes — uma vez no HTML e uma vez no JS bundle.
RSC resolve isso: componentes do servidor enviam HTML renderizado para o cliente, sem JavaScript correspondente. Zero hidratação, zero bundle.
O modelo mental correto
Servidor Cliente
┌─────────────────┐ ┌──────────────────┐
│ Server Component│ ──RSC── │ Client Component │
│ - Acessa banco │ payload│ - useState │
│ - Sem JS bundle │ │ - useEffect │
│ - Sem hooks │ │ - Event handlers │
└─────────────────┘ └──────────────────┘Server Components são o padrão no App Router. Eles:
- Rodam apenas no servidor (build time ou request time)
- Podem acessar banco, filesystem, variáveis de ambiente diretamente
- Não têm estado, não têm efeitos colaterais
- Não aumentam o bundle do cliente
Client Components precisam do marcador 'use client':
- Necessários apenas para interatividade (onClick, onChange, useState)
- São pré-renderizados no servidor (SSR) E hidratados no cliente
Buscando dados direto no componente
// app/posts/page.tsx — Server Component (padrão)
import { db } from '@/lib/db';
// Não precisa de useEffect, fetch, loading states...
export default async function PostsPage() {
// Acesso direto ao banco — isso roda no SERVIDOR
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
include: { author: { select: { name: true } } },
});
return (
<main>
<h1>Posts recentes</h1>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</main>
);
}Nenhum estado de loading. Nenhum useEffect. A página só renderiza quando os dados estão prontos.
Onde Client Components são obrigatórios
'use client'; // necessário para qualquer interatividade
import { useState } from 'react';
export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
return (
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
placeholder="Buscar..."
/>
);
}Regra simples: se usa useState, useEffect, useRef, event handlers ou contexto, precisa de 'use client'.
O padrão de composição correto
// ✅ Correto: Server Component passa dados para Client Component
// app/dashboard/page.tsx (Server Component)
import { UserTable } from './UserTable';
import { db } from '@/lib/db';
export default async function DashboardPage() {
const users = await db.user.findMany();
return <UserTable users={users} />;
// ↑ Server passa dado serializado para o Client
}
// components/UserTable.tsx (Client Component)
'use client';
export function UserTable({ users }) {
const [selected, setSelected] = useState<string[]>([]);
// ... lógica de seleção interativa
}// ❌ Errado: importar Server Component dentro de Client Component
'use client';
import { ServerOnlyComponent } from './ServerOnly'; // erro de build!
A regra: Client Components podem receber Server Components como children (props), mas não podem importá-los diretamente.
Streaming com Suspense
// app/analytics/page.tsx
import { Suspense } from 'react';
import { SlowChart } from './SlowChart';
import { FastStats } from './FastStats';
export default function AnalyticsPage() {
return (
<div>
{/* Renderiza imediatamente */}
<Suspense fallback={<StatsSkeleton />}>
<FastStats />
</Suspense>
{/* Streama quando pronto (pode demorar 2-3s) */}
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
</div>
);
}O browser recebe o HTML em chunks progressivos — o usuário vê conteúdo imediatamente, sem esperar a parte lenta.
Erros comuns na migração
1. Marcar tudo com 'use client' por precaução
Desfaz o benefício de RSC. Use apenas onde necessário.
2. Passar funções não-serializáveis como props para Client Components
// ❌ Erro: funções não são serializáveis
<ClientComponent onSave={() => db.save(data)} />
// ✅ Correto: Server Action
<ClientComponent onSave={saveAction} />
// onde saveAction é uma async function com 'use server'
3. Confundir async Server Component com Client Component com loading state
Se o dado vem do servidor, use Server Component async. Reserve loading states para dados que mudam no cliente após o carregamento inicial.
O App Router com RSC não é mais complicado — é diferente. O modelo mental de “tudo é um componente de cliente” do React clássico não se aplica aqui.