Mergulho em React Server Components: O Adeus ao useEffect?
Se você está acompanhando o ecossistema React, especialmente com a chegada do Next.js 13 e 14, deve ter sentido o impacto. Não estamos falando de mais um hook ou uma lib de state management da moda. Estamos falando de React Server Components (RSC), e, honestamente, isso muda tudo.
A transição tem sido, no mínimo, confusa para muita gente. Erros de "serialização" pipocando, a fronteira entre "cliente" e "servidor" ficando rígida, e nós tendo que reaprender o básico de como buscar dados. O mais chocante? Aquele useEffect que a gente usava pra tudo — desde buscar dados de usuário até trackear analytics — foi "rebaixado".
O useEffect morreu? Calma, não. Mas para buscar dados, ele está respirando por aparelhos.
Neste artigo, vamos descer o nível. Nada de tutorial básico "Hello World". Vamos entender como o RSC funciona por baixo do capô, o que é esse tal protocolo "Flight" e por que esse novo modelo mental resolve problemas arquiteturais que a gente nem sabia que tinha.
1. O Legado: Client-Side Waterfalls
Para valorizar o RSC, precisamos admitir que o nosso jeito "padrão" de fazer SPAs tinha defeitos graves.
Numa SPA clássica/CSR, o navegador baixa um bundle JS gigante. O React boota, monta a árvore de componentes e só aí, lá no useEffect, a gente lembra de pedir os dados.
// Código que a gente escreveu a vida toda function UserProfile({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // 1. Renderiza primeiro, busca dados depois useEffect(() => { fetch(`/api/users/${userId}`).then(data => { setUser(data); setLoading(false); }); }, [userId]); if (loading) return <Spinner />; return ( <div> <h1>{user.name}</h1> {/* 2. Esse componente aqui fica BLOQUEADO esperando o pai */} <UserPosts userId={userId} /> </div> ); }
O problema é o <UserPosts />. Ele é "refém" do pai. Ele só começa a existir (e a buscar os posts dele) depois que o UserProfile terminar tudo.
- Load JS.
UserProfilerender -> Fetch user.- Espera... -> Chega user -> Re-render.
UserPostsmount -> Fetch posts.- Espera... -> Chega posts -> Re-render.
Isso é o famoso Waterfall (Cascata). E o usuário paga o preço olhando pra loaders sequenciais.
React Query e SWR ajudaram muito a gerenciar o estado disso, mas não resolviam a raiz do problema: A lógica do que buscar está no componente, mas a execução depende do navegador do cliente.
2. React Server Components: O Servidor Entra no Jogo
A ideia do RSC é simples e brilhante: "Se os dados estão no servidor, por que não renderizamos o componente lá mesmo?"
Server Components têm acesso direto ao backend. Podem ler arquivos, fazer queries no banco SQL, chamar gRPC interno. Tudo isso sem expor segredos e sem mandar código pro cliente.
Olha a diferença:
// app/users/[id]/page.tsx import db from '@/lib/db'; // 1. Componente async natural async function UserProfile({ params }) { // 2. Query direta no DB. Adeus fetch('/api/...') const user = await db.user.findUnique({ where: { id: params.id } }); return ( <div> <h1>{user.name}</h1> <UserPosts userId={params.id} /> </div> ); } // Outro componente async async function UserPosts({ userId }) { const posts = await db.post.findMany({ where: { authorId: userId } }); return ( <ul> {posts.map(post => <li key={post.id}>{post.title}</li>)} </ul> ); }
Percebe a limpeza?
- Sem
useState, SemuseEffect: Zero gerenciamento de estado de loading manual. async/await: Fluxo de código sequencial e fácil de ler.
Quando o servidor renderiza UserProfile, ele bate no await, pega os dados e segue. Chega no UserPosts, pega os dados e segue. Tudo isso acontece em milissegundos dentro do data center. Latência quase zero entre o componente e o banco.
3. "Flight": O Protocolo Secreto
Muitos confundem RSC com SSR (Server Side Rendering). Não é a mesma coisa.
O SSR devolve HTML puro. O RSC devolve uma estrutura de dados serializada chamada "Flight".
Se você inspecionar a rede, vai ver algo tipo:
1:I["./src/components/ClientCounter.js",["234","345"],"ClientCounter"] 0:["$","div",null,{"children":[["$","h1",null,{"children":"Olá Mundo"}],["$","$L1",null,{}]]}]
O servidor diz pro React no navegador:
- "Renderiza um
divaqui". - "Põe esse texto no
h1". - "E aqui nesse buraco, encaixa o componente cliente
ClientCounter. Toma aqui a referência dele".
O pulo do gato: O código do Server Component nunca vai pro cliente. Você pode usar uma lib pesada de parseamento de Markdown no servidor, transformar em HTML, e o cliente só recebe o HTML. Custo no bundle: 0KB.
4. A Fronteira: Server vs. Client
Como rodam no servidor, Server Components não têm acesso a APIs do navegador (window, localstorage) e não podem ter interatividade (onClick, onChange).
Se você precisa de um botão ou um input, você precisa avisar o React: "Ei, manda isso aqui pro navegador". É aí que entra o "use client".
// src/components/LikeButton.tsx 'use client'; // 👈 A diretiva mágica import { useState } from 'react'; export default function LikeButton() { const [likes, setLikes] = useState(0); return <button onClick={() => setLikes(likes + 1)}>Curtir {likes}</button>; }
Importante: "use client" não transforma o componente em uma SPA antiga. Ele ainda é pré-renderizado no servidor (HTML inicial). A diretiva só diz pro bundler: "Inclua o JS desse cara no pacote pra ele poder ser hidratado".
O Poder da Composição
"Se eu colocar um Context Provider no layout raiz com 'use client', eu mato o SSR?"
Mata não, se você usar o padrão children.
// app/page.tsx (Server Component) import ClientLayout from './ClientLayout'; import ServerPostList from './ServerPostList'; export default function Page() { return ( // Passando o Server Component como filho <ClientLayout> <ServerPostList /> </ClientLayout> ); }
// app/ClientLayout.tsx (Client Component) 'use client'; export default function ClientLayout({ children }) { // 'children' aqui já chega pronto (serializado) do servidor. // O ClientLayout não executa o código do ServerPostList, só exibe o resultado. return <div className="theme-dark">{children}</div>; }
Isso permite que você tenha "ilhas" de interatividade (ClientLayout) sem sacrificar o acesso direto ao banco de dados nos componentes filhos (ServerPostList).
5. Conclusão: Deixe o useEffect ir
Usávamos useEffect para data fetching por falta de opção. Era uma gambiarra aceitável.
Mas convenhamos:
- É lento: Render -> Effect -> Fetch -> Wait -> Re-render.
- Buguento: Race conditions, memory leaks, strict mode rodando duas vezes...
Com RSC, o modelo é síncrono (via await).
Você pede, espera no servidor, e entrega pronto.
Então, na próxima vez que for abrir um useEffect pra dar um fetch, para e pensa:
"Eu não podia resolver isso com um simples await no servidor?"
Dica para quem tá migrando pro App Router: Não tenta reescrever tudo de uma vez. Começa pelas pontas (botões, inputs, cards interativos) transformando em Client Components ('use client') e deixa as páginas em si como Server Components. É o caminho mais suave.