Voltar para o Blog

React Server Components: Guia Completo e Prático Para 2025

Olá HaWkers, React Server Components (RSC) deixaram de ser uma feature experimental para se tornarem o padrão de facto no desenvolvimento React moderno. Com Next.js 15 consolidando o modelo e outros frameworks adotando a tecnologia, entender RSC profundamente é essencial para qualquer desenvolvedor frontend em 2025.

Neste guia, vamos explorar não apenas o "como", mas principalmente o "quando" e o "porquê" de usar Server Components.

O Que São Server Components

React Server Components são componentes que executam exclusivamente no servidor, nunca sendo enviados para o navegador do usuário. Isso representa uma mudança fundamental no modelo mental de como construímos aplicações React.

Modelo Mental Tradicional vs RSC

React Tradicional (Client-Side Rendering):

  1. Usuário acessa a página
  2. Servidor envia HTML mínimo + JavaScript
  3. JavaScript baixa, parseia e executa
  4. React "hidrata" a página, tornando-a interativa
  5. Dados são buscados via API (useEffect, React Query, etc.)
  6. Componentes re-renderizam com dados

React Server Components:

  1. Usuário acessa a página
  2. Servidor executa componentes e busca dados
  3. Servidor envia HTML renderizado + JavaScript mínimo
  4. Apenas componentes interativos são hidratados
  5. Página já chega pronta e funcional

A Grande Diferença

// ❌ Componente Cliente Tradicional
'use client'

import { useState, useEffect } from 'react';

export function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Fetch acontece no CLIENTE após render inicial
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <ProductSkeleton />;

  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  );
}
// ✅ Server Component
// Não precisa de 'use client' - é server por padrão

import { db } from '@/lib/database';

export async function ProductList() {
  // Fetch acontece no SERVIDOR antes de enviar HTML
  const products = await db.products.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' }
  });

  return (
    <ul>
      {products.map(p => <ProductCard key={p.id} product={p} />)}
    </ul>
  );
}

Benefícios Concretos

Vamos quantificar os benefícios de Server Components com exemplos reais.

Redução de Bundle Size

// Análise de bundle típica

// Aplicação e-commerce com React tradicional
const clientBundleTraditional = {
  react: '42kb',
  reactDom: '130kb',
  reactQuery: '35kb',
  axios: '14kb',
  zustand: '8kb',
  dateFormats: '72kb', // date-fns, moment, etc.
  markdown: '45kb',    // para renderizar descrições
  syntax: '180kb',     // highlight.js para code blocks
  total: '526kb gzipped'
};

// Mesma aplicação com RSC
const clientBundleRSC = {
  react: '42kb',
  reactDom: '130kb',
  // reactQuery: não precisa para data fetching
  // axios: não precisa, fetch no servidor
  zustand: '8kb',      // ainda necessário para estado cliente
  // dateFormats: renderizado no servidor
  // markdown: renderizado no servidor
  // syntax: renderizado no servidor
  total: '180kb gzipped' // 66% menor!
};

Performance de Carregamento

// Métricas reais de uma aplicação migrada para RSC

// Antes (CSR)
const metricsCSR = {
  TTFB: '180ms',
  FCP: '1.8s',
  LCP: '3.2s',
  TTI: '4.1s',
  CLS: 0.12,
  bundleSize: '450kb'
};

// Depois (RSC)
const metricsRSC = {
  TTFB: '220ms',      // Ligeiramente maior (renderização servidor)
  FCP: '0.9s',        // 50% mais rápido
  LCP: '1.4s',        // 56% mais rápido
  TTI: '1.8s',        // 56% mais rápido
  CLS: 0.02,          // 83% melhor (menos layout shift)
  bundleSize: '180kb' // 60% menor
};

Quando Usar Server vs Client Components

A decisão entre Server e Client Components deve ser baseada em critérios claros.

Use Server Components Quando

// ✅ Fetching de dados
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findUnique({ where: { id: userId } });
  return <ProfileCard user={user} />;
}

// ✅ Acesso a recursos do servidor
async function ConfigPanel() {
  const config = await readFile('./config.json', 'utf-8');
  return <ConfigDisplay config={JSON.parse(config)} />;
}

// ✅ Renderização de conteúdo pesado
import { marked } from 'marked';
import hljs from 'highlight.js';

async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);
  const html = marked(post.content, {
    highlight: (code, lang) => hljs.highlight(code, { language: lang }).value
  });

  return <article dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ Dados sensíveis
async function AdminDashboard() {
  // Secrets nunca vão para o cliente
  const analytics = await fetchAnalytics(process.env.ANALYTICS_SECRET);
  return <DashboardCharts data={analytics} />;
}

Use Client Components Quando

'use client'

// ✅ Interatividade com estado
import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Cliques: {count}
    </button>
  );
}

// ✅ Efeitos e lifecycle
import { useEffect } from 'react';

export function AnalyticsTracker() {
  useEffect(() => {
    trackPageView();
  }, []);

  return null;
}

// ✅ Event handlers
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
  return (
    <input
      type="search"
      onChange={(e) => onSearch(e.target.value)}
      placeholder="Buscar..."
    />
  );
}

// ✅ APIs do navegador
export function LocationDisplay() {
  const [coords, setCoords] = useState<GeolocationCoordinates | null>(null);

  useEffect(() => {
    navigator.geolocation.getCurrentPosition(
      (pos) => setCoords(pos.coords)
    );
  }, []);

  return coords ? (
    <span>Lat: {coords.latitude}, Lng: {coords.longitude}</span>
  ) : null;
}

// ✅ Hooks customizados com estado
import { useLocalStorage } from '@/hooks/useLocalStorage';

export function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Tema: {theme}
    </button>
  );
}

Padrões de Composição

A arte de RSC está em compor Server e Client Components de forma eficiente.

O Padrão "Wrapper"

// Server Component que envolve Client Component
// page.tsx (Server Component)

import { ProductFilters } from './ProductFilters'; // Client
import { db } from '@/lib/database';

export default async function ProductsPage() {
  // Dados buscados no servidor
  const categories = await db.categories.findMany();
  const brands = await db.brands.findMany();

  return (
    <div>
      <h1>Produtos</h1>
      {/* Client Component recebe dados do servidor como props */}
      <ProductFilters
        categories={categories}
        brands={brands}
      />
      {/* Resto da página é Server Component */}
      <ProductGrid />
    </div>
  );
}
// ProductFilters.tsx
'use client'

import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

interface Props {
  categories: Category[];
  brands: Brand[];
}

export function ProductFilters({ categories, brands }: Props) {
  const router = useRouter();
  const searchParams = useSearchParams();
  const [selectedCategory, setSelectedCategory] = useState(
    searchParams.get('category') ?? ''
  );

  function handleCategoryChange(categoryId: string) {
    setSelectedCategory(categoryId);
    const params = new URLSearchParams(searchParams);
    params.set('category', categoryId);
    router.push(`/products?${params.toString()}`);
  }

  return (
    <aside>
      <h3>Filtros</h3>
      <select
        value={selectedCategory}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="">Todas categorias</option>
        {categories.map(cat => (
          <option key={cat.id} value={cat.id}>{cat.name}</option>
        ))}
      </select>
      {/* Mais filtros... */}
    </aside>
  );
}

O Padrão "Slot"

// Server Component que aceita Client Components como children

// Modal.tsx (Server Component)
interface ModalProps {
  title: string;
  children: React.ReactNode; // Pode ser Client ou Server
}

export function Modal({ title, children }: ModalProps) {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        <h2>{title}</h2>
        {children}
      </div>
    </div>
  );
}

// Uso em page.tsx
import { Modal } from './Modal';
import { InteractiveForm } from './InteractiveForm'; // Client Component

export default function Page() {
  return (
    <Modal title="Novo Produto">
      <InteractiveForm /> {/* Client Component dentro de Server */}
    </Modal>
  );
}

O Padrão "Island"

// page.tsx - Majoritariamente Server com "ilhas" de interatividade

import { Suspense } from 'react';
import { LikeButton } from './LikeButton';        // Client
import { CommentSection } from './CommentSection'; // Client
import { ShareMenu } from './ShareMenu';           // Client

export default async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);
  const author = await getAuthor(post.authorId);

  return (
    <article>
      {/* Conteúdo estático - Server */}
      <header>
        <h1>{post.title}</h1>
        <AuthorCard author={author} />
        <time>{formatDate(post.publishedAt)}</time>
      </header>

      {/* Conteúdo renderizado - Server */}
      <div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />

      {/* Ilhas de interatividade - Client */}
      <footer>
        <LikeButton postId={post.id} initialLikes={post.likes} />
        <ShareMenu url={`/blog/${slug}`} title={post.title} />
      </footer>

      {/* Seção interativa com loading state */}
      <Suspense fallback={<CommentSkeleton />}>
        <CommentSection postId={post.id} />
      </Suspense>
    </article>
  );
}

Streaming e Suspense

RSC habilitam streaming de HTML, melhorando significativamente a experiência percebida.

Streaming Básico

// page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>

      {/* Renderiza imediatamente */}
      <QuickStats />

      {/* Streama quando pronto */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart /> {/* Async Server Component */}
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders /> {/* Async Server Component */}
      </Suspense>

      <Suspense fallback={<ListSkeleton />}>
        <TopProducts /> {/* Async Server Component */}
      </Suspense>
    </div>
  );
}

// Cada componente async busca dados independentemente
async function RevenueChart() {
  const data = await fetchRevenueData(); // Pode demorar 2s
  return <Chart data={data} />;
}

async function RecentOrders() {
  const orders = await fetchRecentOrders(); // Pode demorar 1s
  return <OrdersTable orders={orders} />;
}

async function TopProducts() {
  const products = await fetchTopProducts(); // Pode demorar 500ms
  return <ProductsList products={products} />;
}

Parallel Data Fetching

// ✅ Correto: Fetches em paralelo
async function Dashboard() {
  // Inicia todos os fetches simultaneamente
  const revenuePromise = fetchRevenue();
  const ordersPromise = fetchOrders();
  const productsPromise = fetchProducts();

  // Aguarda todos
  const [revenue, orders, products] = await Promise.all([
    revenuePromise,
    ordersPromise,
    productsPromise
  ]);

  return (
    <div>
      <RevenueChart data={revenue} />
      <OrdersTable data={orders} />
      <ProductsList data={products} />
    </div>
  );
}

// ❌ Errado: Fetches sequenciais (waterfall)
async function DashboardSlow() {
  const revenue = await fetchRevenue();    // Espera 1s
  const orders = await fetchOrders();      // Depois espera mais 1s
  const products = await fetchProducts();  // Depois mais 500ms
  // Total: 2.5s

  return (/* ... */);
}

// Com Promise.all: 1s (o mais lento)
// Sem Promise.all: 2.5s (soma de todos)

Server Actions

Server Actions complementam RSC permitindo mutações de forma elegante.

Básico de Server Actions

// actions.ts
'use server'

import { revalidatePath } from 'next/cache';
import { db } from '@/lib/database';

export async function createProduct(formData: FormData) {
  const name = formData.get('name') as string;
  const price = parseFloat(formData.get('price') as string);

  await db.products.create({
    data: { name, price }
  });

  revalidatePath('/products');
}

export async function deleteProduct(productId: string) {
  await db.products.delete({
    where: { id: productId }
  });

  revalidatePath('/products');
}
// ProductForm.tsx - Pode ser Server Component!
import { createProduct } from './actions';

export function ProductForm() {
  return (
    <form action={createProduct}>
      <input name="name" placeholder="Nome do produto" required />
      <input name="price" type="number" step="0.01" required />
      <button type="submit">Criar Produto</button>
    </form>
  );
}

Server Actions com Validação

// actions.ts
'use server'

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const ProductSchema = z.object({
  name: z.string().min(3).max(100),
  price: z.number().positive(),
  description: z.string().optional(),
  categoryId: z.string().uuid()
});

type ActionResult = {
  success: boolean;
  error?: string;
  data?: any;
};

export async function createProduct(formData: FormData): Promise<ActionResult> {
  const rawData = {
    name: formData.get('name'),
    price: parseFloat(formData.get('price') as string),
    description: formData.get('description'),
    categoryId: formData.get('categoryId')
  };

  const validation = ProductSchema.safeParse(rawData);

  if (!validation.success) {
    return {
      success: false,
      error: validation.error.errors[0].message
    };
  }

  try {
    const product = await db.products.create({
      data: validation.data
    });

    revalidatePath('/products');

    return {
      success: true,
      data: product
    };
  } catch (error) {
    return {
      success: false,
      error: 'Erro ao criar produto'
    };
  }
}
// ProductForm.tsx
'use client'

import { useFormState, useFormStatus } from 'react-dom';
import { createProduct } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Criando...' : 'Criar Produto'}
    </button>
  );
}

export function ProductForm() {
  const [state, formAction] = useFormState(createProduct, {
    success: false
  });

  return (
    <form action={formAction}>
      {state.error && (
        <div className="error">{state.error}</div>
      )}

      <input name="name" placeholder="Nome" required />
      <input name="price" type="number" step="0.01" required />
      <textarea name="description" placeholder="Descrição" />

      <SubmitButton />

      {state.success && (
        <div className="success">Produto criado com sucesso!</div>
      )}
    </form>
  );
}

Erros Comuns e Como Evitar

Aprenda com os erros mais frequentes ao trabalhar com RSC.

Erro 1: Importar Client Component sem 'use client'

// ❌ Erro comum
// Button.tsx - Esqueceu 'use client'
import { useState } from 'react';

export function Button() {
  const [clicked, setClicked] = useState(false);
  // Erro: useState não funciona em Server Components
}

// ✅ Correto
// Button.tsx
'use client'

import { useState } from 'react';

export function Button() {
  const [clicked, setClicked] = useState(false);
  // Funciona!
}

Erro 2: Passar Funções Como Props Para Client Components

// ❌ Erro
// page.tsx (Server Component)
export default function Page() {
  function handleClick() {
    console.log('clicked');
  }

  // Erro: funções não são serializáveis
  return <ClientButton onClick={handleClick} />;
}

// ✅ Correto - Use Server Actions
// page.tsx
import { handleAction } from './actions';

export default function Page() {
  return <ClientButton action={handleAction} />;
}

// actions.ts
'use server'
export async function handleAction() {
  console.log('action executed');
}

Erro 3: Usar Hooks em Server Components

// ❌ Erro
// ServerComponent.tsx
import { useRouter } from 'next/navigation';

export function ServerComponent() {
  const router = useRouter(); // Erro: hooks não funcionam aqui
}

// ✅ Correto - Mova para Client Component
// Navegação via redirect em Server Components
import { redirect } from 'next/navigation';

export async function ServerComponent() {
  const shouldRedirect = await checkCondition();
  if (shouldRedirect) {
    redirect('/other-page');
  }
}

Migração de Projetos Existentes

Estratégias para migrar gradualmente para RSC.

Abordagem Incremental

// 1. Identifique componentes "folha" que não precisam de interatividade

// Antes: Client Component desnecessário
'use client'
export function ProductCard({ product }) {
  return (
    <div className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
}

// Depois: Server Component (remova 'use client')
export function ProductCard({ product }) {
  return (
    <div className="card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{formatCurrency(product.price)}</p> {/* Pode usar libs pesadas */}
    </div>
  );
}

Checklist de Migração

## Migração para RSC

### Fase 1: Análise
- [ ] Identificar componentes sem estado/efeitos
- [ ] Mapear dependências de cada componente
- [ ] Identificar data fetching patterns atuais

### Fase 2: Preparação
- [ ] Atualizar Next.js para versão 13.4+
- [ ] Configurar App Router
- [ ] Criar estrutura de pastas /app

### Fase 3: Migração Gradual
- [ ] Converter layouts para Server Components
- [ ] Mover data fetching para o servidor
- [ ] Marcar componentes interativos com 'use client'
- [ ] Substituir useEffect + fetch por async components
- [ ] Implementar Suspense boundaries

### Fase 4: Otimização
- [ ] Implementar streaming onde apropriado
- [ ] Adicionar loading.tsx para rotas
- [ ] Configurar cache e revalidação
- [ ] Implementar Server Actions para forms

Conclusão

React Server Components representam a maior evolução na arquitetura React desde os Hooks. Eles resolvem problemas reais de performance e experiência do usuário que desenvolvedores enfrentam há anos.

O segredo para dominar RSC está em entender claramente a divisão de responsabilidades: Server Components para dados e renderização pesada, Client Components para interatividade. Com essa mentalidade, você construirá aplicações mais rápidas, mais simples e mais escaláveis.

Se você quer aprofundar seus conhecimentos em React moderno, recomendo que dê uma olhada em outro artigo: React Hooks Avançados: Patterns e Otimização onde você vai descobrir técnicas que complementam o uso de Server Components.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário