Next.js App Router + shadcn/ui: guia para combinar Server e Client Components

Uma mensagem de erro aparece na tela: Error: You're importing a component that needs useEffect. It only works in a Client Component but none of its parents are marked with "use client".
Você já adicionou "use client" ao layout.tsx. Então por que o erro continua?
Depois de vasculhar a documentação, você descobre que o problema está no limite definido pela cadeia de importações dos componentes. No App Router, a separação entre Server Components e Client Components é muito mais complexa do que parece.
Esse é um cenário real que muitos desenvolvedores enfrentam ao migrar para o App Router. Por padrão, todos os componentes são Server Components, mas a maior parte das bibliotecas de UI, como o shadcn/ui, precisa de Client Components. Como definir esse limite? Como os dados devem circular? E como otimizar o desempenho?
Este artigo explica cada um desses pontos.
Server Components vs. Client Components: a diferença fundamental
Primeiro, o princípio mais básico: no App Router, todos os componentes são Server Components por padrão.
O que isso significa? Seus arquivos page.tsx e layout.tsx são renderizados no servidor por padrão e não enviam nenhum código JavaScript ao navegador.
O que os Server Components podem fazer
A principal vantagem dos Server Components é estarem “mais perto dos dados”:
// app/products/page.tsx - Server Component (padrão)
async function ProductsPage() {
// Aguarda os dados diretamente no componente
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 } // Cache por 1 hora
}).then(res => res.json())
return (
<div>
{products.map(p => (
<div key={p.id}>{p.name} - ${p.price}</div>
))}
</div>
)
}
Observe: não há useEffect nem useState. Você obtém os dados diretamente com await. Essa é a característica de “componente assíncrono” dos Server Components.
Quando usar:
- Busca de dados (fetch, consultas ao banco de dados)
- Acesso a APIs exclusivas do backend (headers(), cookies())
- Bibliotecas pesadas (por exemplo, um parser de Markdown com mais de 100 KB, que não precisa ser empacotado para o navegador quando usado em um Server Component)
- Tratamento de informações sensíveis (a API key nunca fica exposta no frontend)
O que os Client Components podem fazer
Client Components são os “componentes React tradicionais” que já conhecemos. Basta adicionar "use client" no topo do arquivo:
// components/like-button.tsx
'use client'
import { useState } from 'react'
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false)
const [count, setCount] = useState(0)
const handleClick = () => {
setLiked(!liked)
setCount(prev => liked ? prev - 1 : prev + 1)
}
return (
<button onClick={handleClick}>
{liked ? '❤️' : '🤍'} {count}
</button>
)
}
Quando usar:
- Tratamento de eventos (onClick, onChange, onSubmit)
- Hooks do React (useState, useEffect, useRef, useContext)
- APIs do navegador (localStorage, window, document)
- Context Provider
Há um detalhe que parece contraintuitivo: os Client Components também têm seu HTML pré-renderizado no servidor. Depois, eles passam pelo processo de hydrate no navegador para recuperar a capacidade de interação. Portanto, na primeira visita, o usuário ainda vê o conteúdo completo, sem encarar uma “tela em branco enquanto o JavaScript carrega”.
Regra central: quem pode importar quem
É aqui que os erros aparecem com mais frequência.
A regra é simples, mas muita gente a memoriza ao contrário:
- Um Server Component pode importar um Client Component ✅
- Um Client Component não pode importar um Server Component ❌
- Um Server Component pode ser passado como children para um Client Component ✅
A terceira regra pode parecer confusa. O código deixa isso mais claro:
// app/page.tsx - Server Component
import { ClientContainer } from './client-container'
import { ServerData } from './server-data'
export default function Page() {
return (
<ClientContainer>
{/* ServerData é passado como children */}
<ServerData />
</ClientContainer>
)
}
// client-container.tsx
'use client'
export function ClientContainer({ children }) {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children}
</div>
)
}
// server-data.tsx - Server Component
async function ServerData() {
const data = await fetch('/api/data').then(r => r.json())
return <div>{data.title}</div>
}
Esse padrão é muito comum: Client Container cuida da lógica de interação, enquanto Server Data busca os dados. Os dois ficam isolados por children, sem uma importação direta do Server Component pelo Client Component.
Integração com shadcn/ui: por que parece tão “complicada”
O shadcn/ui é uma das minhas bibliotecas de UI favoritas, mas usá-lo no App Router realmente exige alguns cuidados.
O motivo é simples: o shadcn/ui é baseado no Radix UI, e a maioria dos componentes usa hooks do React.
Componentes como Button, Dialog e Dropdown Menu usam internamente useState ou useEffect. Por isso, precisam ser Client Components.
Exemplo incorreto: usar shadcn/ui diretamente em um Server Component
// ❌ Incorreto: Server Component importa Client Component
import { Button } from '@/components/ui/button'
async function ProductPage() {
const product = await fetchProduct()
return (
<div>
<h1>{product.name}</h1>
{/* Isso gera um erro: Button precisa de "use client" */}
<Button onClick={() => addToCart(product.id)}>
Add to Cart
</Button>
</div>
)
}
A mensagem informa que Button usa useState e precisa ser marcado com "use client".
Solução correta 1: extraia a parte interativa para um Client Component
Esta é a solução mais comum e mais simples:
// app/product/page.tsx - Server Component
import { ProductInfo } from './product-info'
import { AddToCartButton } from './add-to-cart-button'
async function ProductPage({ params }) {
const product = await fetchProduct(params.id)
return (
<div>
{/* Server Component: responsável por exibir os dados */}
<ProductInfo product={product} />
{/* Client Component: responsável pela interação */}
<AddToCartButton productId={product.id} />
</div>
)
}
// product-info.tsx - Server Component
export function ProductInfo({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>${product.price}</span>
</div>
)
}
// add-to-cart-button.tsx - Client Component
'use client'
import { Button } from '@/components/ui/button'
import { useState } from 'react'
export function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false)
const handleAdd = async () => {
setLoading(true)
await addToCart(productId)
setLoading(false)
}
return (
<Button onClick={handleAdd} disabled={loading}>
{loading ? 'Adding...' : 'Add to Cart'}
</Button>
)
}
A ideia central é extrair apenas a parte que precisa de interação para um nó folha e manter todo o restante como Server Component.
Solução correta 2: padrão de composição (Server passa dados para Client)
Quando o Client Component precisa de dados iniciais:
// app/dashboard/page.tsx - Server Component
import { DataTable } from './data-table'
async function DashboardPage() {
const users = await fetchUsers() // Server Component busca os dados
return <DataTable data={users} /> // Passa para o Client Component
}
// data-table.tsx - Client Component
'use client'
import { Table } from '@/components/ui/table'
import { useState } from 'react'
export function DataTable({ data }) {
const [selectedRows, setSelectedRows] = useState([])
return (
<Table>
{/* Componente Table do shadcn/ui */}
<TableBody>
{data.map(user => (
<TableRow
key={user.id}
selected={selectedRows.includes(user.id)}
onClick={() => toggleSelection(user.id)}
>
<TableCell>{user.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
Assim, você aproveita a capacidade de buscar dados dos Server Components sem abrir mão da interatividade dos Client Components.
Onde colocar o Context Provider
Outra dúvida comum: onde devem ficar os Context Providers globais, como ThemeProvider e AuthProvider?
A resposta: eles precisam ficar em um Client Component, mas o mais profundamente possível na árvore.
// app/layout.tsx - Server Component (root layout)
export default function RootLayout({ children }) {
return (
<html>
<body>
{/* Não coloque o Provider aqui */}
{children}
</body>
</html>
)
}
// app/providers.tsx - Client Component
'use client'
import { ThemeProvider } from 'next-themes'
import { AuthProvider } from './auth-context'
export function Providers({ children }) {
return (
<ThemeProvider>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
)
}
// app/dashboard/layout.tsx - Server Component
import { Providers } from '../providers'
export default function DashboardLayout({ children }) {
return (
<Providers>
{children}
</Providers>
)
}
Por que colocá-lo em um nível “profundo”? Porque o Provider faz com que todos os componentes que envolve façam parte da subárvore de Client Components. Se ele ficar no root layout, toda a aplicação será forçada a renderizar no cliente.
Ao colocá-lo em um nível mais profundo, como o layout de uma rota específica, você minimiza o alcance do Provider.
Fluxo de dados: do Server para o Client
Props são a maneira mais simples e confiável:
// Server Component busca os dados
const data = await fetchData()
// Passa para o Client Component
<ClientComponent initialData={data} />
Mas há um recurso importante para otimizar o desempenho: a função React.cache().
Se vários Server Components precisarem dos mesmos dados, você pode usar cache para evitar requisições repetidas:
// lib/get-user.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
return await db.query('SELECT * FROM users WHERE id = ?', [id])
})
// app/layout.tsx
async function Layout() {
const user = await getUser('123') // Primeira requisição
return <header>{user.name}</header>
}
// app/page.tsx
async function Page() {
const user = await getUser('123') // Mesmo argumento, sem repetir a requisição
return <main>Welcome {user.name}</main>
}
Durante um único ciclo de renderização, cache deduplica automaticamente as chamadas que usam os mesmos argumentos.
Os quatro erros mais comuns
Erro 1: usar “use client” em um nível muito alto
// ❌ app/layout.tsx recebeu "use client"
'use client'
export default function Layout({ children }) {
return <div>{children}</div>
}
Isso transforma toda a subárvore da aplicação em Client Components e elimina as vantagens de desempenho dos Server Components.
Correção: adicione "use client" apenas aos componentes que realmente precisam de interação e mantenha-os nos nós folha.
Erro 2: usar hooks em um Server Component
// ❌ Server Component usa useState
async function Page() {
const [count, setCount] = useState(0) // Erro!
return <div>{count}</div>
}
Correção: extraia a parte que precisa de hooks para um Client Component.
Erro 3: usar headers()/cookies() em um Client Component
// ❌ Client Component usa uma API do servidor
'use client'
import { headers } from 'next/headers'
function UserProfile() {
const headersList = headers() // Erro! Só pode ser usado em Server Component
return <div>...</div>
}
Correção: busque os dados no Server Component e passe-os ao Client Component:
// Server Component obtém os headers
async function Page() {
const userAgent = headers().get('user-agent')
return <UserProfile userAgent={userAgent} />
}
// Client Component recebe os dados
'use client'
function UserProfile({ userAgent }) {
return <div>Browser: {userAgent}</div>
}
Erro 4: componente de terceiros sem “use client”
// ❌ Server Component importa componente de terceiros sem marcação
import { AcmeCarousel } from 'acme-carousel'
async function Page() {
return <AcmeCarousel /> // Erro! AcmeCarousel usa hooks internamente
}
Correção: crie um wrapper:
// components/carousel-wrapper.tsx
'use client'
import { AcmeCarousel } from 'acme-carousel'
export function CarouselWrapper(props) {
return <AcmeCarousel {...props} />
}
// page.tsx - Server Component
import { CarouselWrapper } from './carousel-wrapper'
async function Page() {
return <CarouselWrapper /> // Funciona normalmente
}
Recomendações para otimizar o desempenho
Por fim, algumas dicas práticas:
1. Coloque os Client Components nos nós folha
Essa regra pode reduzir em 70% o JavaScript enviado ao cliente.
Em uma página com uma lista de produtos, por exemplo:
- Grade de produtos: Server Component
- Cada card de produto: Server Component
- Seletor de quantidade do card: Client Component (a única parte interativa)
2. Use Suspense para renderização em streaming
// app/page.tsx
import { Suspense } from 'react'
import { ProductList } from './product-list'
import { Recommendations } from './recommendations'
export default function Page() {
return (
<div>
{/* Exibe o esqueleto primeiro e o substitui quando os dados chegam */}
<Suspense fallback={<ProductSkeleton />}>
<ProductList />
</Suspense>
{/* Conteúdo secundário renderizado em um stream independente */}
<Suspense fallback={<RecSkeleton />}>
<Recommendations />
</Suspense>
</div>
)
}
O usuário vê primeiro a estrutura da página, e os dados são preenchidos aos poucos. A experiência é muito melhor do que “esperar todos os dados carregarem”.
3. Estratégia de cache do fetch
// Dados estáticos (obtidos durante o build)
await fetch(url, { cache: 'force-cache' })
// ISR: revalidação a cada hora
await fetch(url, { next: { revalidate: 3600 } })
// Dados dinâmicos (obtidos a cada requisição)
await fetch(url, { cache: 'no-store' })
Escolha a estratégia de cache adequada para evitar renderização dinâmica desnecessária.
Resumo
Depois de todos esses detalhes, os princípios centrais são poucos:
- Use Server Components por padrão e Client Components apenas quando houver necessidade de interação
- Server pode importar Client, mas Client não pode importar Server
- Passe dados por children ou props para manter limites claros
- Adicione “use client” nos nós folha, sem usá-lo indiscriminadamente nos níveis mais altos
- Extraia os componentes do shadcn/ui, em vez de misturá-los diretamente em um Server Component
O limite entre Server e Client no App Router foi pensado para deixar o desenvolvedor “mais perto dos dados e mais longe do navegador”. Quando você entende esse princípio, muitas dúvidas desaparecem.
Comece a praticar em uma página simples: primeiro escreva um Server Component que busca os dados e, depois, adicione aos poucos as partes interativas. Quando surgir um erro, não entre em pânico. Quase sempre é um problema de limite entre componentes; verifique a relação entre as importações e você provavelmente encontrará a causa rapidamente.
Série: este artigo faz parte da série Guia completo de Next.js (artigo 46). Se você está aprendendo Next.js App Router, confira os outros artigos da série. Para mais dicas práticas sobre shadcn/ui, leia a série Guia prático de Tailwind e shadcn/ui.
Como combinar Server e Client Components corretamente
Boas práticas para integrar o shadcn/ui a um projeto com Next.js App Router
⏱️ Estimated time: 30 min
- 1
Step 1: Identifique o tipo necessário para cada componente
Determine se cada componente precisa de recursos interativos:
• Precisa tratar eventos (onClick, onChange) → Client Component
• Precisa de hooks do React (useState, useEffect) → Client Component
• Precisa de APIs do navegador (localStorage, window) → Client Component
• Apenas exibe dados, sem interação → Server Component (padrão) - 2
Step 2: Extraia a parte interativa para um nó folha
Extraia apenas a parte que precisa de interação para um Client Component:
• Crie um arquivo e adicione 'use client' no topo
• Importe os componentes do shadcn/ui (Button, Dialog etc.)
• Importe esse Client Component no Server Component
• Passe os dados por props - 3
Step 3: Projete o fluxo de dados
O Server Component busca os dados e os passa ao Client Component:
• Use async/await no Server Component para buscar os dados
• Passe-os ao Client Component por props
• Quando vários lugares precisarem dos mesmos dados, use React.cache() para evitar requisições repetidas
• Evite usar headers()/cookies() diretamente em um Client Component - 4
Step 4: Posicione o Context Provider
O Provider precisa ser um Client Component, mas deve ficar em um layout mais profundo:
• Crie providers.tsx e marque-o com 'use client'
• Envolva ThemeProvider, AuthProvider e outros providers
• Importe-o no layout.tsx de uma rota específica (não no root layout)
• Minimize o alcance da subárvore de Client Components - 5
Step 5: Valide e otimize
Verifique se os limites entre os componentes estão corretos:
• Garanta que 'use client' apareça apenas nos nós folha
• Confirme que nenhum Client Component importa um Server Component
• Envolva componentes assíncronos com Suspense
• Configure uma estratégia de cache adequada para fetch
FAQ
Por que os componentes do shadcn/ui precisam ser Client Components?
Server Components e Client Components podem importar uns aos outros?
Como evitar a repetição da mesma requisição em vários Server Components?
Onde devo colocar um Context Provider?
O que fazer ao encontrar o erro de que useEffect só pode ser usado em um Client Component?
Como decidir se um componente deve ser Server ou Client Component?
11 min de leitura · Publicado em: 31 mar 2026 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Boas práticas de Next.js com Tailwind CSS: da configuração ao modo escuro (2025)
Guia prático de Next.js com Tailwind CSS v4 em 2025 para reduzir classes extensas, configurar temas personalizados, implementar o modo escuro e otimizar o CSS de 500 KB para 50 KB.
Parte 24 de 26
Próximo
Guia completo do SWR: estratégias de cache e atualizações otimistas na prática
Aprenda os conceitos centrais e as estratégias de cache do SWR e simplifique 90% do código de busca de dados com um único Hook. Inclui exemplos de atualização otimista, comparação com React Query e boas práticas de integração com Next.js.
Parte 26 de 26



Comentários
Entre com GitHub para comentar