Saltar para o conteúdo
React Server Components no Next.js 14: O que realmente mudou
React

React Server Components no Next.js 14: O que realmente mudou

8 de novembro de 2024·Paulo de Paula

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.