Voltar para o Blog

React 19 Server Components: Guia Pratico Para Dominar a Nova Era do React

Ola HaWkers, o React 19 trouxe uma mudanca fundamental na forma como construimos aplicacoes web. Os Server Components representam a maior evolucao do React desde a introducao dos Hooks, e entender esse conceito e essencial para qualquer desenvolvedor frontend em 2025.

Voce ja se perguntou por que suas aplicacoes React enviam tanto JavaScript para o navegador? Os Server Components chegaram para resolver exatamente esse problema, permitindo que componentes sejam renderizados no servidor sem enviar codigo desnecessario ao cliente.

O Que Sao Server Components

Server Components sao componentes React que executam exclusivamente no servidor. Diferente dos componentes tradicionais que rodam no navegador, eles nunca sao enviados para o cliente, resultando em bundles menores e melhor performance.

React Server Components

Beneficios Principais

  • Zero JavaScript no cliente: Componentes de servidor nao adicionam ao bundle
  • Acesso direto a recursos: Banco de dados, sistema de arquivos, APIs internas
  • Streaming automatico: Conteudo enviado progressivamente ao usuario
  • SEO otimizado: HTML completo disponivel para crawlers

Client Components vs Server Components

A distincao entre esses dois tipos de componentes e fundamental para arquitetar aplicacoes React modernas.

Server Components (Padrao no React 19)

// app/produtos/page.tsx
// Este e um Server Component por padrao

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

interface Produto {
  id: number;
  nome: string;
  preco: number;
  descricao: string;
}

async function buscarProdutos(): Promise<Produto[]> {
  // Acesso direto ao banco - impossivel em Client Components
  const produtos = await db.query('SELECT * FROM produtos ORDER BY criado_em DESC');
  return produtos;
}

export default async function PaginaProdutos() {
  const produtos = await buscarProdutos();

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">Nossos Produtos</h1>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {produtos.map((produto) => (
          <article key={produto.id} className="border rounded-lg p-4">
            <h2 className="text-xl font-semibold">{produto.nome}</h2>
            <p className="text-gray-600 mt-2">{produto.descricao}</p>
            <p className="text-2xl font-bold mt-4 text-green-600">
              R$ {produto.preco.toFixed(2)}
            </p>
          </article>
        ))}
      </div>
    </main>
  );
}

Client Components (Interatividade)

// components/BotaoCarrinho.tsx
'use client'; // Diretiva obrigatoria para Client Components

import { useState } from 'react';

interface BotaoCarrinhoProps {
  produtoId: number;
  nome: string;
  preco: number;
}

export function BotaoCarrinho({ produtoId, nome, preco }: BotaoCarrinhoProps) {
  const [quantidade, setQuantidade] = useState(1);
  const [adicionado, setAdicionado] = useState(false);

  const handleAdicionar = async () => {
    try {
      await fetch('/api/carrinho', {
        method: 'POST',
        body: JSON.stringify({ produtoId, quantidade }),
        headers: { 'Content-Type': 'application/json' }
      });

      setAdicionado(true);
      setTimeout(() => setAdicionado(false), 2000);
    } catch (error) {
      console.error('Erro ao adicionar ao carrinho:', error);
    }
  };

  return (
    <div className="flex items-center gap-4 mt-4">
      <div className="flex items-center border rounded">
        <button
          onClick={() => setQuantidade(Math.max(1, quantidade - 1))}
          className="px-3 py-1 hover:bg-gray-100"
        >
          -
        </button>
        <span className="px-4">{quantidade}</span>
        <button
          onClick={() => setQuantidade(quantidade + 1)}
          className="px-3 py-1 hover:bg-gray-100"
        >
          +
        </button>
      </div>

      <button
        onClick={handleAdicionar}
        disabled={adicionado}
        className={`px-6 py-2 rounded font-medium transition-colors ${
          adicionado
            ? 'bg-green-500 text-white'
            : 'bg-blue-600 text-white hover:bg-blue-700'
        }`}
      >
        {adicionado ? 'Adicionado!' : 'Adicionar ao Carrinho'}
      </button>
    </div>
  );
}

Composicao: Combinando Server e Client Components

A verdadeira magia acontece quando combinamos os dois tipos de componentes de forma estrategica.

// app/produtos/[id]/page.tsx
// Server Component - busca dados

import { db } from '@/lib/database';
import { BotaoCarrinho } from '@/components/BotaoCarrinho';
import { GaleriaImagens } from '@/components/GaleriaImagens';
import { AvaliacoesClientes } from '@/components/AvaliacoesClientes';

interface PageProps {
  params: { id: string };
}

async function buscarProduto(id: string) {
  const produto = await db.query(
    'SELECT * FROM produtos WHERE id = ?',
    [id]
  );
  return produto[0];
}

async function buscarAvaliacoes(produtoId: string) {
  return await db.query(
    'SELECT * FROM avaliacoes WHERE produto_id = ? ORDER BY data DESC LIMIT 10',
    [produtoId]
  );
}

export default async function PaginaProduto({ params }: PageProps) {
  // Buscas paralelas no servidor
  const [produto, avaliacoes] = await Promise.all([
    buscarProduto(params.id),
    buscarAvaliacoes(params.id)
  ]);

  if (!produto) {
    return <div>Produto nao encontrado</div>;
  }

  return (
    <main className="container mx-auto p-4">
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
        {/* Client Component para interatividade da galeria */}
        <GaleriaImagens imagens={produto.imagens} />

        <div>
          <h1 className="text-3xl font-bold">{produto.nome}</h1>
          <p className="text-gray-600 mt-4">{produto.descricao}</p>

          <div className="mt-6">
            <span className="text-4xl font-bold text-green-600">
              R$ {produto.preco.toFixed(2)}
            </span>
          </div>

          {/* Client Component para adicionar ao carrinho */}
          <BotaoCarrinho
            produtoId={produto.id}
            nome={produto.nome}
            preco={produto.preco}
          />
        </div>
      </div>

      {/* Server Component renderizado com dados do servidor */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Avaliacoes dos Clientes</h2>
        <AvaliacoesClientes avaliacoes={avaliacoes} />
      </section>
    </main>
  );
}

GaleriaImagens como Client Component

// components/GaleriaImagens.tsx
'use client';

import { useState } from 'react';
import Image from 'next/image';

interface GaleriaImagensProps {
  imagens: string[];
}

export function GaleriaImagens({ imagens }: GaleriaImagensProps) {
  const [imagemAtiva, setImagemAtiva] = useState(0);

  return (
    <div className="space-y-4">
      <div className="relative aspect-square bg-gray-100 rounded-lg overflow-hidden">
        <Image
          src={imagens[imagemAtiva]}
          alt="Imagem do produto"
          fill
          className="object-cover"
          priority
        />
      </div>

      <div className="flex gap-2 overflow-x-auto">
        {imagens.map((img, index) => (
          <button
            key={index}
            onClick={() => setImagemAtiva(index)}
            className={`relative w-20 h-20 rounded border-2 overflow-hidden flex-shrink-0 ${
              index === imagemAtiva ? 'border-blue-500' : 'border-transparent'
            }`}
          >
            <Image src={img} alt="" fill className="object-cover" />
          </button>
        ))}
      </div>
    </div>
  );
}

Streaming e Suspense

Uma das grandes vantagens dos Server Components e o streaming de conteudo com Suspense, permitindo que partes da pagina sejam exibidas enquanto outras ainda carregam.

// app/dashboard/page.tsx
import { Suspense } from 'react';
import { EstatisticasGerais } from '@/components/EstatisticasGerais';
import { GraficoVendas } from '@/components/GraficoVendas';
import { TabelaPedidos } from '@/components/TabelaPedidos';
import { LoadingSpinner } from '@/components/LoadingSpinner';

export default function Dashboard() {
  return (
    <main className="p-6 space-y-6">
      <h1 className="text-3xl font-bold">Dashboard</h1>

      {/* Estatisticas carregam primeiro - rapidas */}
      <Suspense fallback={<LoadingSpinner texto="Carregando estatisticas..." />}>
        <EstatisticasGerais />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Grafico pode demorar mais */}
        <Suspense fallback={<LoadingSpinner texto="Carregando grafico..." />}>
          <GraficoVendas />
        </Suspense>

        {/* Tabela independente do grafico */}
        <Suspense fallback={<LoadingSpinner texto="Carregando pedidos..." />}>
          <TabelaPedidos />
        </Suspense>
      </div>
    </main>
  );
}

// components/EstatisticasGerais.tsx
// Server Component - executa no servidor

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

async function buscarEstatisticas() {
  const [vendas, clientes, pedidos] = await Promise.all([
    db.query('SELECT SUM(valor) as total FROM vendas WHERE mes = CURRENT_MONTH'),
    db.query('SELECT COUNT(*) as total FROM clientes WHERE ativo = true'),
    db.query('SELECT COUNT(*) as total FROM pedidos WHERE status = "pendente"')
  ]);

  return {
    vendasMes: vendas[0].total,
    clientesAtivos: clientes[0].total,
    pedidosPendentes: pedidos[0].total
  };
}

export async function EstatisticasGerais() {
  const stats = await buscarEstatisticas();

  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Vendas do Mes</h3>
        <p className="text-3xl font-bold text-green-600">
          R$ {stats.vendasMes.toLocaleString()}
        </p>
      </div>

      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Clientes Ativos</h3>
        <p className="text-3xl font-bold text-blue-600">
          {stats.clientesAtivos.toLocaleString()}
        </p>
      </div>

      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500 text-sm">Pedidos Pendentes</h3>
        <p className="text-3xl font-bold text-orange-600">
          {stats.pedidosPendentes}
        </p>
      </div>
    </div>
  );
}

Server Actions: Mutacoes no Servidor

Server Actions permitem executar funcoes no servidor diretamente de componentes, simplificando drasticamente o tratamento de formularios.

// app/contato/page.tsx
import { enviarMensagem } from './actions';

export default function PaginaContato() {
  return (
    <main className="container mx-auto p-4 max-w-lg">
      <h1 className="text-3xl font-bold mb-6">Entre em Contato</h1>

      <form action={enviarMensagem} className="space-y-4">
        <div>
          <label htmlFor="nome" className="block text-sm font-medium mb-1">
            Nome
          </label>
          <input
            type="text"
            id="nome"
            name="nome"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            Email
          </label>
          <input
            type="email"
            id="email"
            name="email"
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <div>
          <label htmlFor="mensagem" className="block text-sm font-medium mb-1">
            Mensagem
          </label>
          <textarea
            id="mensagem"
            name="mensagem"
            rows={5}
            required
            className="w-full border rounded-lg px-4 py-2"
          />
        </div>

        <BotaoEnviar />
      </form>
    </main>
  );
}

// Botao com estado de loading
'use client';

import { useFormStatus } from 'react-dom';

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

  return (
    <button
      type="submit"
      disabled={pending}
      className="w-full bg-blue-600 text-white py-3 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50"
    >
      {pending ? 'Enviando...' : 'Enviar Mensagem'}
    </button>
  );
}
// app/contato/actions.ts
'use server';

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

export async function enviarMensagem(formData: FormData) {
  const nome = formData.get('nome') as string;
  const email = formData.get('email') as string;
  const mensagem = formData.get('mensagem') as string;

  // Validacao no servidor
  if (!nome || !email || !mensagem) {
    throw new Error('Todos os campos sao obrigatorios');
  }

  if (!email.includes('@')) {
    throw new Error('Email invalido');
  }

  // Salvar no banco
  await db.query(
    'INSERT INTO mensagens (nome, email, mensagem, criado_em) VALUES (?, ?, ?, NOW())',
    [nome, email, mensagem]
  );

  // Enviar email de notificacao
  await fetch(process.env.EMAIL_API_URL!, {
    method: 'POST',
    body: JSON.stringify({
      to: process.env.ADMIN_EMAIL,
      subject: `Nova mensagem de ${nome}`,
      body: mensagem
    })
  });

  // Revalidar cache e redirecionar
  revalidatePath('/contato');
  redirect('/contato/sucesso');
}

Padroes de Cache e Revalidacao

Entender o sistema de cache do React 19 e Next.js e crucial para aplicacoes performaticas.

// lib/data.ts

// Cache por tempo - revalida a cada hora
export async function buscarCategorias() {
  const response = await fetch('https://api.exemplo.com/categorias', {
    next: { revalidate: 3600 } // 1 hora em segundos
  });
  return response.json();
}

// Cache estatico - revalida apenas em build
export async function buscarPaginasEstaticas() {
  const response = await fetch('https://api.exemplo.com/paginas', {
    cache: 'force-cache'
  });
  return response.json();
}

// Sem cache - sempre busca dados frescos
export async function buscarNotificacoes() {
  const response = await fetch('https://api.exemplo.com/notificacoes', {
    cache: 'no-store'
  });
  return response.json();
}

// Cache com tags para invalidacao seletiva
export async function buscarProdutosPorCategoria(categoriaId: string) {
  const response = await fetch(
    `https://api.exemplo.com/categorias/${categoriaId}/produtos`,
    {
      next: {
        revalidate: 300, // 5 minutos
        tags: [`categoria-${categoriaId}`, 'produtos']
      }
    }
  );
  return response.json();
}
// app/admin/produtos/actions.ts
'use server';

import { revalidateTag, revalidatePath } from 'next/cache';

export async function atualizarProduto(produtoId: string, dados: FormData) {
  await db.query('UPDATE produtos SET ... WHERE id = ?', [produtoId]);

  // Invalida cache especifico
  revalidateTag(`produto-${produtoId}`);
  revalidateTag('produtos');

  // Ou invalida por caminho
  revalidatePath('/produtos');
  revalidatePath(`/produtos/${produtoId}`);
}

export async function deletarProduto(produtoId: string) {
  await db.query('DELETE FROM produtos WHERE id = ?', [produtoId]);

  // Invalida todos os caches relacionados a produtos
  revalidateTag('produtos');
  revalidatePath('/produtos');
}

Erros Comuns e Como Evitar

Alguns erros sao frequentes ao trabalhar com Server Components pela primeira vez.

Erro 1: Usar Hooks em Server Components

// ERRADO - hooks nao funcionam em Server Components
export default async function Pagina() {
  const [estado, setEstado] = useState(''); // Erro!
  // ...
}

// CORRETO - extrair para Client Component
'use client';
export function ComponenteInterativo() {
  const [estado, setEstado] = useState('');
  // ...
}

Erro 2: Passar Funcoes como Props para Client Components

// ERRADO - funcoes nao sao serializaveis
export default function ServerComponent() {
  const handleClick = () => console.log('click');

  return <ClientComponent onClick={handleClick} />; // Erro!
}

// CORRETO - usar Server Actions
// actions.ts
'use server';
export async function handleSubmit(formData: FormData) {
  // ...
}

// page.tsx
import { handleSubmit } from './actions';

export default function ServerComponent() {
  return (
    <form action={handleSubmit}>
      {/* ... */}
    </form>
  );
}

Se voce quer dominar mais tecnicas avancadas de frontend, recomendo conferir o artigo CSS Moderno em 2025 onde exploramos as novidades que complementam perfeitamente o React.

Bora pra cima! 🦅

Comentários (0)

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

Adicionar comentário