React 19: Server Components em producao - O guia completo

Dominar os Server Components do React 19 em producao. Arquitetura, padroes, streaming, caching e otimizacoes para aplicacoes de alta performance.

Ilustracao dos Server Components do React 19 mostrando a arquitetura servidor-cliente interconectada

Os Server Components representam a evolucao mais significativa do React desde os Hooks. Com o React 19, essa arquitetura atingiu a maturidade necessaria para producao, permitindo que componentes sejam executados diretamente no servidor sem comprometer a interatividade no lado do cliente.

Pre-requisitos

Este guia pressupoe familiaridade com React e Next.js App Router. Os exemplos utilizam Next.js 14+, que implementa React Server Components de forma nativa.

Entendendo a arquitetura dos Server Components

Os Server Components (RSC) introduzem um novo paradigma: alguns componentes rodam exclusivamente no servidor, outros no cliente, e ambos podem coexistir na mesma arvore de componentes. Essa separacao otimiza drasticamente a performance ao reduzir o bundle de JavaScript enviado ao navegador.

A ideia fundamental se baseia no fato de que muitos componentes nao precisam de interatividade. Um componente que exibe uma lista de artigos a partir de um banco de dados, por exemplo, pode rodar inteiramente no servidor. Apenas elementos interativos (botoes, formularios, animacoes) necessitam de JavaScript no lado do cliente.

app/articles/page.tsxtsx
// This component runs only on the server
// No JavaScript is sent to the client for this component
import { getArticles } from '@/lib/articles'
import ArticleCard from './ArticleCard'
import LikeButton from './LikeButton'

// async/await directly in the component
// Only possible with Server Components
export default async function ArticlesPage() {
  // Direct database call (no REST API needed)
  const articles = await getArticles()

  return (
    <main className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-6">Recent Articles</h1>

      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {articles.map((article) => (
          // ArticleCard is also a Server Component
          <ArticleCard key={article.id} article={article}>
            {/* LikeButton is a Client Component (interactive) */}
            <LikeButton articleId={article.id} />
          </ArticleCard>
        ))}
      </div>
    </main>
  )
}

A diretiva "use client" marca explicitamente os componentes que precisam de JavaScript no navegador.

app/articles/LikeButton.tsxtsx
'use client'

// useState and interactive hooks require "use client"
import { useState, useTransition } from 'react'
import { likeArticle } from '@/actions/articles'

interface LikeButtonProps {
  articleId: string
  initialLikes?: number
}

export default function LikeButton({ articleId, initialLikes = 0 }: LikeButtonProps) {
  // Local state for optimistic UI
  const [likes, setLikes] = useState(initialLikes)
  const [isPending, startTransition] = useTransition()

  const handleLike = () => {
    // Immediate optimistic update
    setLikes((prev) => prev + 1)

    // Server Action to persist
    startTransition(async () => {
      await likeArticle(articleId)
    })
  }

  return (
    <button
      onClick={handleLike}
      disabled={isPending}
      className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
    >
      <span>❤️</span>
      <span>{likes}</span>
    </button>
  )
}

Essa arquitetura reduz significativamente o bundle de JavaScript: apenas o codigo do LikeButton e enviado ao cliente, nao o do ArticlesPage nem o do ArticleCard.

Padroes de composicao Server/Client

A composicao entre Server Components e Client Components segue regras precisas. Um Server Component pode importar e renderizar Client Components, mas o inverso nao e possivel de forma direta. Para passar conteudo do servidor a um componente cliente, o padrao children oferece a solucao.

components/InteractiveWrapper.tsxtsx
'use client'

import { useState, ReactNode } from 'react'

interface InteractiveWrapperProps {
  children: ReactNode
  expandable?: boolean
}

// Client Component that wraps server content
export function InteractiveWrapper({ children, expandable = false }: InteractiveWrapperProps) {
  const [isExpanded, setIsExpanded] = useState(!expandable)

  if (!expandable) {
    return <div>{children}</div>
  }

  return (
    <div className="border rounded-lg overflow-hidden">
      <button
        onClick={() => setIsExpanded(!isExpanded)}
        className="w-full p-4 text-left font-medium bg-gray-50 hover:bg-gray-100"
      >
        {isExpanded ? '▼ Collapse' : '▶ Expand'}
      </button>

      {isExpanded && (
        <div className="p-4">
          {/* children can contain Server Components */}
          {children}
        </div>
      )}
    </div>
  )
}
app/dashboard/page.tsxtsx
// Server Component using the client wrapper
import { InteractiveWrapper } from '@/components/InteractiveWrapper'
import { getStats, getRecentActivity } from '@/lib/dashboard'

export default async function DashboardPage() {
  // Parallel server-side requests
  const [stats, activity] = await Promise.all([
    getStats(),
    getRecentActivity()
  ])

  return (
    <div className="space-y-6">
      {/* Stats in an expandable wrapper */}
      <InteractiveWrapper expandable>
        {/* This content is rendered server-side then passed to client */}
        <div className="grid grid-cols-3 gap-4">
          <StatCard title="Users" value={stats.users} />
          <StatCard title="Revenue" value={stats.revenue} />
          <StatCard title="Orders" value={stats.orders} />
        </div>
      </InteractiveWrapper>

      {/* Recent activity */}
      <InteractiveWrapper>
        <ActivityList activities={activity} />
      </InteractiveWrapper>
    </div>
  )
}

Esse padrao permite combinar a interatividade do cliente com dados renderizados no servidor sem duplicar logica.

Obtencao de dados e caching

Fetch estendido pelo React

O React 19 estende automaticamente a API nativa fetch para adicionar deduplicacao e caching. Requisicoes identicas dentro do mesmo render sao executadas apenas uma vez.

A obtencao de dados em Server Components acontece diretamente com async/await. O React cuida automaticamente da deduplicacao de requisicoes identicas.

lib/api.tstsx
// Centralized request configuration with caching
const API_BASE = process.env.API_URL

// Request with time-based revalidation
export async function getProducts() {
  const response = await fetch(`${API_BASE}/products`, {
    // Revalidate every hour
    next: { revalidate: 3600 }
  })

  if (!response.ok) {
    throw new Error('Failed to fetch products')
  }

  return response.json()
}

// Request without cache (real-time data)
export async function getCurrentUser() {
  const response = await fetch(`${API_BASE}/me`, {
    // No cache, always fresh
    cache: 'no-store'
  })

  if (!response.ok) {
    return null
  }

  return response.json()
}

// Request with tag for targeted invalidation
export async function getProduct(id: string) {
  const response = await fetch(`${API_BASE}/products/${id}`, {
    next: {
      tags: [`product-${id}`],
      revalidate: 3600
    }
  })

  if (!response.ok) {
    throw new Error('Product not found')
  }

  return response.json()
}

Para acesso direto ao banco de dados (Prisma, Drizzle), o React cache com unstable_cache oferece as mesmas capacidades.

lib/db-queries.tstsx
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'

// Cache categories (rarely modified)
export const getCategories = unstable_cache(
  async () => {
    return prisma.category.findMany({
      orderBy: { name: 'asc' }
    })
  },
  ['categories'], // Cache key
  {
    revalidate: 86400, // 24 hours
    tags: ['categories']
  }
)

// Cache products by category
export const getProductsByCategory = unstable_cache(
  async (categoryId: string) => {
    return prisma.product.findMany({
      where: { categoryId },
      include: { images: true },
      orderBy: { createdAt: 'desc' }
    })
  },
  ['products-by-category'],
  {
    revalidate: 3600,
    tags: ['products']
  }
)

// Cache invalidation after mutation
export async function createProduct(data: ProductInput) {
  const product = await prisma.product.create({ data })

  // Invalidate related caches
  revalidateTag('products')

  return product
}

Pronto para mandar bem nas entrevistas de React / Next.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Streaming e Suspense para uma UX otima

O streaming permite enviar HTML progressivamente ao navegador, exibindo imediatamente as partes disponiveis enquanto outras continuam carregando. Combinado com Suspense, esse mecanismo melhora drasticamente o Time to First Byte (TTFB) e a experiencia percebida pelo usuario.

app/product/[id]/page.tsxtsx
import { Suspense } from 'react'
import { getProduct } from '@/lib/products'
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import RecommendedProducts from './RecommendedProducts'
import { Skeleton } from '@/components/ui/Skeleton'

interface ProductPageProps {
  params: { id: string }
}

export default async function ProductPage({ params }: ProductPageProps) {
  // This request blocks initial render
  const product = await getProduct(params.id)

  return (
    <div className="container mx-auto py-8">
      {/* Immediate render with product data */}
      <ProductDetails product={product} />

      {/* Reviews load via streaming */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Customer Reviews</h2>
        <Suspense fallback={<ReviewsSkeleton />}>
          {/* This async component will be streamed */}
          <ProductReviews productId={params.id} />
        </Suspense>
      </section>

      {/* Recommendations too */}
      <section className="mt-12">
        <h2 className="text-2xl font-bold mb-6">Similar Products</h2>
        <Suspense fallback={<ProductGridSkeleton count={4} />}>
          <RecommendedProducts productId={params.id} />
        </Suspense>
      </section>
    </div>
  )
}

// Skeleton for reviews
function ReviewsSkeleton() {
  return (
    <div className="space-y-4">
      {[1, 2, 3].map((i) => (
        <div key={i} className="border rounded-lg p-4">
          <Skeleton className="h-4 w-32 mb-2" />
          <Skeleton className="h-4 w-full" />
          <Skeleton className="h-4 w-3/4" />
        </div>
      ))}
    </div>
  )
}
app/product/[id]/ProductReviews.tsxtsx
// Async Server Component that will be streamed
import { getProductReviews } from '@/lib/reviews'

interface ProductReviewsProps {
  productId: string
}

export default async function ProductReviews({ productId }: ProductReviewsProps) {
  // This request may take time
  // Component will be streamed when complete
  const reviews = await getProductReviews(productId)

  if (reviews.length === 0) {
    return (
      <p className="text-gray-500 italic">
        No reviews yet. Be the first to share your thoughts!
      </p>
    )
  }

  return (
    <div className="space-y-4">
      {reviews.map((review) => (
        <article key={review.id} className="border rounded-lg p-4">
          <div className="flex items-center gap-2 mb-2">
            <span className="font-medium">{review.author}</span>
            <span className="text-yellow-500">
              {'★'.repeat(review.rating)}{'☆'.repeat(5 - review.rating)}
            </span>
          </div>
          <p className="text-gray-700">{review.content}</p>
          <time className="text-sm text-gray-500">
            {new Date(review.createdAt).toLocaleDateString('en-US')}
          </time>
        </article>
      ))}
    </div>
  )
}

O navegador recebe primeiro o esqueleto HTML com os skeletons, e depois o conteudo real e injetado progressivamente via streaming.

Server Actions para mutacoes

Os Server Actions permitem executar codigo do servidor a partir de componentes cliente sem necessidade de criar rotas de API. Essa abordagem simplifica consideravelmente o tratamento de mutacoes.

actions/cart.tstsx
'use server'

import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { getCurrentUser } from '@/lib/auth'

// Action to add to cart
export async function addToCart(productId: string, quantity: number = 1) {
  const user = await getCurrentUser()

  if (!user) {
    // Return structured error
    return { error: 'Login required', code: 'UNAUTHORIZED' }
  }

  try {
    // Check stock
    const product = await prisma.product.findUnique({
      where: { id: productId }
    })

    if (!product || product.stock < quantity) {
      return { error: 'Insufficient stock', code: 'OUT_OF_STOCK' }
    }

    // Add or update cart item
    await prisma.cartItem.upsert({
      where: {
        cartId_productId: {
          cartId: user.cartId,
          productId
        }
      },
      update: {
        quantity: { increment: quantity }
      },
      create: {
        cartId: user.cartId,
        productId,
        quantity
      }
    })

    // Invalidate cart page cache
    revalidatePath('/cart')

    return { success: true, message: 'Product added to cart' }
  } catch (error) {
    console.error('Add to cart error:', error)
    return { error: 'An error occurred', code: 'SERVER_ERROR' }
  }
}

// Action to remove from cart
export async function removeFromCart(itemId: string) {
  const user = await getCurrentUser()

  if (!user) {
    return { error: 'Login required' }
  }

  await prisma.cartItem.delete({
    where: { id: itemId, cart: { userId: user.id } }
  })

  revalidatePath('/cart')

  return { success: true }
}
components/AddToCartButton.tsxtsx
'use client'

import { useTransition } from 'react'
import { addToCart } from '@/actions/cart'
import { toast } from '@/components/ui/toast'

interface AddToCartButtonProps {
  productId: string
}

export function AddToCartButton({ productId }: AddToCartButtonProps) {
  const [isPending, startTransition] = useTransition()

  const handleClick = () => {
    startTransition(async () => {
      const result = await addToCart(productId)

      if (result.error) {
        toast.error(result.error)
        return
      }

      toast.success(result.message)
    })
  }

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      className="w-full py-3 px-6 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
    >
      {isPending ? (
        <span className="flex items-center justify-center gap-2">
          <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
          Adding...
        </span>
      ) : (
        'Add to Cart'
      )}
    </button>
  )
}
Validacao no lado do servidor

Os dados devem ser sempre validados nos Server Actions. Validacoes do lado do cliente podem ser contornadas. Recomenda-se usar Zod ou uma biblioteca similar para uma validacao robusta.

Tratamento de erros e Error Boundaries

O React 19 aprimora o tratamento de erros com Server Components. Os Error Boundaries funcionam da mesma forma que com Client Components.

app/products/error.tsxtsx
'use client'

// Error Boundary for /products segment
interface ErrorProps {
  error: Error & { digest?: string }
  reset: () => void
}

export default function ProductsError({ error, reset }: ErrorProps) {
  return (
    <div className="flex flex-col items-center justify-center min-h-[400px] text-center">
      <div className="p-6 bg-red-50 rounded-lg max-w-md">
        <h2 className="text-xl font-bold text-red-800 mb-2">
          Loading Error
        </h2>
        <p className="text-red-600 mb-4">
          Unable to load products. Please try again.
        </p>
        {/* Display digest for debugging */}
        {error.digest && (
          <p className="text-sm text-gray-500 mb-4">
            Reference: {error.digest}
          </p>
        )}
        <button
          onClick={reset}
          className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
        >
          Try Again
        </button>
      </div>
    </div>
  )
}
app/products/loading.tsxtsx
// Loading UI during initial load
export default function ProductsLoading() {
  return (
    <div className="container mx-auto py-8">
      <div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
      <div className="grid grid-cols-3 gap-6">
        {[1, 2, 3, 4, 5, 6].map((i) => (
          <div key={i} className="border rounded-lg p-4">
            <div className="aspect-square bg-gray-200 rounded animate-pulse mb-4" />
            <div className="h-4 bg-gray-200 rounded animate-pulse mb-2" />
            <div className="h-4 w-2/3 bg-gray-200 rounded animate-pulse" />
          </div>
        ))}
      </div>
    </div>
  )
}

Para um tratamento de erros mais granular em componentes especificos, o componente ErrorBoundary pode ser utilizado diretamente.

components/ErrorBoundary.tsxtsx
'use client'

import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // Log error to monitoring service
    console.error('ErrorBoundary caught:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-4 bg-red-50 rounded-lg">
          <p className="text-red-600">An error occurred</p>
        </div>
      )
    }

    return this.props.children
  }
}

Otimizacoes de performance em producao

Diversas tecnicas permitem otimizar a performance dos Server Components em producao.

app/layout.tsxtsx
import { Suspense } from 'react'
import { headers } from 'next/headers'

// Preload critical data
export const dynamic = 'force-dynamic'

export default async function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* Header streamed independently */}
        <Suspense fallback={<HeaderSkeleton />}>
          <Header />
        </Suspense>

        <main>{children}</main>

        {/* Footer can be static */}
        <Footer />
      </body>
    </html>
  )
}
lib/prefetch.tstsx
// Data prefetching for links
import { preload } from 'react-dom'

export function prefetchProduct(productId: string) {
  // Prefetch images
  preload(`/api/products/${productId}/image`, { as: 'image' })
}

// Usage in a component
// <Link href={`/products/${id}`} onMouseEnter={() => prefetchProduct(id)}>
next.config.jstsx
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Build optimizations
  experimental: {
    // Enable Partial Prerendering (PPR)
    ppr: true,
    // Optimize package imports
    optimizePackageImports: ['lucide-react', '@radix-ui/react-icons']
  },

  // Image configuration
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { hostname: 'cdn.example.com' }
    ]
  }
}

module.exports = nextConfig

Pronto para mandar bem nas entrevistas de React / Next.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusao

Os Server Components do React 19 transformam de forma fundamental a maneira como aplicacoes React sao construidas. Os pontos-chave a reter:

  • Separacao servidor/cliente: usar "use client" apenas para componentes interativos
  • Obtencao direta de dados: async/await nos componentes, sem necessidade de useEffect ou rotas de API
  • Streaming com Suspense: exibicao progressiva para uma melhor experiencia percebida
  • Server Actions: mutacoes simplificadas sem criar endpoints de API
  • Caching inteligente: revalidate e tags para otimizar a performance
  • Composicao flexivel: padrao children para combinar Server e Client Components

Essa arquitetura permite criar aplicacoes mais performaticas com menos JavaScript no lado do cliente, simplificando significativamente o codigo. A transicao para Server Components representa um investimento que se paga rapidamente em termos de performance e manutenibilidade.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#react 19
#server components
#rsc
#performance
#next.js

Compartilhar

Artigos relacionados