Back

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.

  1. Load JS.
  2. UserProfile render -> Fetch user.
  3. Espera... -> Chega user -> Re-render.
  4. UserPosts mount -> Fetch posts.
  5. 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?

  1. Sem useState, Sem useEffect: Zero gerenciamento de estado de loading manual.
  2. 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 div aqui".
  • "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:

  1. É lento: Render -> Effect -> Fetch -> Wait -> Re-render.
  2. 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.

ReactNext.jsServer ComponentsPerformanceWeb Development