Padrões de Composição
Combine Server Components e Client Components de forma eficaz usando o padrão children, slots e arquitetura ciente de limites.
Busque em todas as páginas da documentação
Combine Server Components e Client Components de forma eficaz usando o padrão children, slots e arquitetura ciente de limites.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida -- pronto para copiar e colar.
// Padrão 1: Passar Server Components como children para Client Components
// app/components/client-sidebar.tsx
"use client";
import { useState } from "react";
export function Sidebar({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(true);
return (
<aside className={open ? "w-64" : "w-0"}>
<button onClick={() => setOpen(!open)}>Toggle</button>
{open && children}
</aside>
);
}
// app/page.tsx (Server Component)
import { Sidebar } from "./components/client-sidebar";
import { ServerNav } from "./components/server-nav";
export default function Page() {
return (
<Sidebar>
{/* ServerNav renderiza no servidor, passado como JSX pré-renderizado */}
<ServerNav />
</Sidebar>
);
}Quando usar isso: Você precisa de um Client Component (para interatividade) que envolva ou contenha Server Components (para busca de dados ou renderização zero-JS).
// app/components/accordion.tsx
"use client";
import { useState } from "react";
type AccordionProps = {
title: string;
children: React.ReactNode;
};
export function Accordion({ title, children }: AccordionProps) {
const [expanded, setExpanded] = useState(false);
return (
<div className="border rounded mb-2">
<button
onClick={() => setExpanded(!expanded)}
className="w-full text-left px-4 py-3 font-medium flex justify-between"
>
{title}
<span>{expanded ? "-" : "+"}</span>
</button>
{expanded && <div className="px-4 pb-4">{children}</div>}
</div>
);
}// app/components/product-details.tsx (Server Component -- sem diretiva)
import { db } from "@/lib/db";
export async function ProductDetails({ productId }: { productId: string }) {
const product = await db.product.findUnique({
where: { id: productId },
include: { specs: true },
});
if (!product) return <p>Produto não encontrado</p>;
return (
<dl className="grid grid-cols-2 gap-2 text-sm">
{product.specs.map((spec) => (
<div key={spec.id}>
<dt className="font-medium text-gray-600">{spec.label}</dt>
<dd>{spec.value}</dd>
</div>
))}
</dl>
);
}// app/components/product-reviews.tsx (Server Component)
import { db } from "@/lib/db";
export async function ProductReviews({ productId }: { productId: string }) {
const reviews = await db.review.findMany({
where: { productId },
orderBy: { createdAt: "desc" },
take: 5,
});
return (
<ul className="space-y-3">
{reviews.map((r) => (
<li key={r.id} className="border-b pb-3">
<p className="font-medium">{r.author}</p>
<p className="text-gray-600">{r.body}</p>
</li>
))}
</ul>
);
}// app/products/[id]/page.tsx (Server Component orquestra tudo)
import { Suspense } from "react";
import { Accordion } from "@/app/components/accordion";
import { ProductDetails } from "@/app/components/product-details";
import { ProductReviews } from "@/app/components/product-reviews";
import { AddToCartButton } from "@/app/components/add-to-cart";
type Props = { params: Promise<{ id: string }> };
export default async function ProductPage({ params }: Props) {
const { id } = await params;
return (
<main className="max-w-2xl mx-auto p-6">
<Accordion title="Especificações">
{/* Server Component renderizado no servidor, passado como children */}
<Suspense fallback={<p>Carregando especificações...</p>}>
<ProductDetails productId={id} />
</Suspense>
</Accordion>
<Accordion title="Avaliações">
<Suspense fallback={<p>Carregando avaliações...</p>}>
<ProductReviews productId={id} />
</Suspense>
</Accordion>
{/* Client Component para interatividade */}
<AddToCartButton productId={id} />
</main>
);
}// app/components/add-to-cart.tsx
"use client";
import { useTransition } from "react";
import { addToCart } from "@/app/actions/cart";
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() => startTransition(() => addToCart(productId))}
disabled={isPending}
className="mt-4 w-full bg-blue-600 text-white py-3 rounded font-medium disabled:opacity-50"
>
{isPending ? "Adicionando..." : "Adicionar ao Carrinho"}
</button>
);
}O que isso demonstra:
Accordion) envolve Server Components (ProductDetails, ProductReviews) via props childrenawait e são renderizados no servidor; sua saída é passada como JSX pré-renderizado para o Client ComponentSuspense permite streaming independente de dados do servidor"use client" torna tudo em sua árvore de dependência exclusivo para o cliente.children ou qualquer outra prop React.ReactNode. O Server Component já está renderizado no servidor; o Client Component recebe JSX pré-renderizado, não uma referência de módulo."use client". Tudo acima dele (na árvore de importação) é do servidor; tudo abaixo é do cliente.children).Padrão de slots múltiplos:
// Client Component com slots nomeados
"use client";
export function DashboardLayout({
sidebar,
header,
children,
}: {
sidebar: React.ReactNode;
header: React.ReactNode;
children: React.ReactNode;
}) {
const [collapsed, setCollapsed] = useState(false);
return (
<div className="flex">
<aside className={collapsed ? "w-16" : "w-64"}>{sidebar}</aside>
<div className="flex-1">
<header>{header}</header>
<main>{children}</main>
</div>
</div>
);
}// Orquestrador Server Component
import { DashboardLayout } from "./dashboard-layout";
import { ServerSidebar } from "./server-sidebar";
import { ServerHeader } from "./server-header";
export default async function DashboardPage() {
return (
<DashboardLayout
sidebar={<ServerSidebar />}
header={<ServerHeader />}
>
<ServerMainContent />
</DashboardLayout>
);
}Padrão de provedores de contexto:
// app/providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class">
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ThemeProvider>
);
}// app/layout.tsx (Server Component)
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Extraindo partes interativas para um Client Component fino:
// Em vez de tornar o card inteiro um Client Component...
// Extraia apenas a parte interativa
"use client";
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "Curtido" : "Curtir"}
</button>
);
}
// Mantenha o card como um Server Component
export default async function PostCard({ id }: { id: string }) {
const post = await fetchPost(id);
return (
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
<LikeButton postId={id} />
</article>
);
}// Use React.ReactNode para qualquer prop que receba JSX do servidor
type LayoutProps = {
children: React.ReactNode;
sidebar: React.ReactNode;
modal: React.ReactNode;
};
// A saída do Server Component é JSX já renderizado -- não uma referência de componente
// TypeScript o trata como ReactNode, que inclui JSX.Element, string, number, etc.
// Props de Server Actions são válidas através do limite
type FormProps = {
submitAction: (formData: FormData) => Promise<{ error?: string }>;
};Importar um Server Component em um arquivo "use client" -- A importação converte silenciosamente o Server Component em um Client Component. Nenhum erro é lançado, mas ele perde o comportamento exclusivo do servidor. Correção: Passe-o como children ou uma prop JSX de um componente pai Server Component.
Provedores de contexto devem ser Client Components -- O Contexto do React requer "use client". Mas colocar provedores no layout torna o layout um componente cliente e todos os children se tornam clientes também. Correção: Crie um arquivo providers.tsx separado com "use client" e envolva {children} no layout do Server Component.
Uso excessivo de "use client" -- Marcar um componente de alto nível como "use client" puxa todos os children para o bundle do cliente. Correção: Empurre o limite "use client" o mais baixo possível na árvore de componentes. Extraia apenas as peças interativas.
Passar props não serializáveis -- Passar uma função, instância de classe ou Symbol de um Server Component para um Client Component falha silenciosamente ou lança um erro. Correção: Passe apenas dados serializáveis. Use Server Actions para comportamento semelhante a funções.
Estado compartilhado entre servidor e cliente -- Não há estado compartilhado. Server Components rodam no servidor; Client Components hidratam no cliente. Correção: Passe os dados iniciais como props do servidor para o cliente. Use Server Actions para sincronizar o estado de volta.
| Padrão | Use Quando | Não Use Quando |
|---|---|---|
| Padrão Children | Client Component envolve a saída de Server Component | Todos os children já são exclusivos para o cliente |
| Props de Slot (sidebar, header) | Múltiplas regiões independentes de Server Component | Uma única prop children é suficiente |
| Wrapper de provedores de contexto | Você precisa de Contexto React na raiz sem tornar o layout um Client Component | Nenhum contexto é necessário |
dynamic(import, { ssr: false }) | Uma biblioteca de terceiros não pode renderizar no servidor de forma alguma | SSR normal + hidratação funcionam bem |
| Server Actions como props | Um Client Component precisa acionar lógica do lado do servidor | A interação é puramente do lado do cliente |
children (ou outra prop JSX) para um Client Component.O limite "use client" torna tudo em sua árvore de dependência exclusivo para o cliente. Importar um Server Component dentro de um arquivo "use client" o converte silenciosamente em um Client Component, perdendo todo o comportamento exclusivo do servidor.
Um Client Component aceita múltiplas props React.ReactNode (por exemplo, sidebar, header, children). Um orquestrador Server Component passa diferentes Server Components para cada slot:
<DashboardLayout
sidebar={<ServerSidebar />}
header={<ServerHeader />}
>
<ServerMainContent />
</DashboardLayout>Crie um arquivo providers.tsx separado com "use client" que envolva {children}. Importe-o no seu layout Server Component:
// app/layout.tsx (Server Component)
import { Providers } from "./providers";
export default function RootLayout({ children }) {
return (
<html><body>
<Providers>{children}</Providers>
</body></html>
);
}Todos os children e importações desse componente são puxados para o bundle do cliente. Isso anula o propósito dos Server Components. Correção: empurre o limite "use client" o mais baixo possível e extraia apenas as peças interativas.
children ou uma prop JSX de um componente pai Server Component em vez disso.Não. Funções regulares, instâncias de classe e Symbols não são serializáveis e falharão. Use Server Actions (funções assíncronas com "use server") para comportamento semelhante a funções através do limite.
Use React.ReactNode para qualquer prop que receba JSX do servidor:
type LayoutProps = {
children: React.ReactNode;
sidebar: React.ReactNode;
modal: React.ReactNode;
};type FormProps = {
submitAction: (formData: FormData) => Promise<{ error?: string }>;
};Server Actions são as únicas funções que podem cruzar o limite servidor-cliente como props.
Suspense permite que seu conteúdo faça streaming independentemente.Não. Server Components rodam no servidor; Client Components hidratam no cliente. Passe os dados iniciais como props do servidor para o cliente. Use Server Actions para sincronizar o estado de volta para o servidor.
Mantenha o componente de busca de dados como um Server Component e extraia apenas a peça interativa (por exemplo, um botão) em um pequeno Client Component:
// Server Component
export default async function PostCard({ id }) {
const post = await fetchPost(id);
return (
<article>
<h2>{post.title}</h2>
<LikeButton postId={id} /> {/* Client Component */}
</article>
);
}Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥