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: fetch direto 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, crypto nã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

  1. Dados: buscar no servidor (RSC)
  2. Estado: gerenciar interatividade mínima (Client Components)
  3. 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