App Router: Server Components vs Client Components
You know the App Router gives you file-based routing. What it doesn't advertise as loudly is that every component you write inside it starts life as a Server Component — and that single default reshapes how you're supposed to build a Next.js app.
01Server Components are the default, not an opt-in
In the App Router, every component is a Server Component unless you say otherwise. That's a reversal from the mental model most React developers grew up with, where everything renders in the browser and "server-side rendering" was something added on top.
A Server Component runs — as the name says — only on the server. It renders to HTML (or a special streaming format) before anything reaches the browser, and by default it ships zero JavaScript for that component to the client. There's no hydration cost for it, because there's nothing to hydrate: it never runs again in the browser.
What a Server Component can do
Read environment variables and secrets directly, without exposing them to the client
Query a database or call an internal API directly inside the component, with no separate API route needed
await data straight in the component body — no useEffect, no loading state to manage by hand
Import large libraries (a markdown renderer, a date library) without adding a single byte to the browser bundle
What a Server Component cannot do
Use useState, useEffect, or any hook that depends on the component being alive in the browser
Attach event handlers like onClick or onChange
Touch browser-only APIs: window, localStorage, geolocation, etc.
02Client Components: the explicit opt-in
The moment a component needs interactivity, state, or a browser API, you mark it with a single line at the very top of the file: "use client". That directive doesn't mean "this only runs in the browser" — it means "this component (and everything it imports) needs to be included in the JavaScript bundle sent to the browser, because it has to run there too."
Client Components still render once on the server for the initial HTML (so the page isn't blank on first load), then hydrate in the browser and become interactive from that point on — the same mechanics React has always used, just now scoped to only the components that actually need it.
03The boundary is the real architectural decision
Marking a file "use client" doesn't just affect that file — it affects everything it imports. Once you cross into client territory, everything downstream in that import tree ships to the browser too. That's why the recommended pattern is to push "use client" as far down the tree as possible: keep layout, data-fetching, and static content as Server Components, and isolate the interactive bit — a button, a form, a dropdown — into its own small Client Component.
Aspect
Server Component
Client Component
Where it runs
Server only
Server (first render) + browser
JS sent to browser
None
Yes, its whole bundle
Hooks (useState, useEffect)
Not allowed
Allowed
Direct data access (DB, secrets)
Yes
No — needs to receive data as props or fetch via an API
Browser APIs, event handlers
Not allowed
Allowed
Default in the App Router
Yes
Opt-in via "use client"
Keep as Server Component
Page layouts, headers, footers with no interactivity
Anything that fetches data to display (a list, an article, a dashboard's static parts)
Markdown/content rendering, formatting, anything CPU-heavy but non-interactive
Mark as Client Component
Forms, buttons, toggles, anything with onClick/onChange
Components using useState, useEffect, or context
Anything touching window, browser storage, or third-party client-only libraries
04Practical example: mixing both in one page
A product page can fetch its data on the server and pass only what's needed down to a small interactive piece — say, an "add to cart" button:
// app/products/[id]/page.jsx — Server Component (default, no directive needed)
import AddToCartButton from "./add-to-cart-button";
export default async function ProductPage({ params }) {
const product = await getProductFromDatabase(params.id);
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</article>
);
}
The page itself, the fetch, and the product text never reach the browser as JavaScript. Only the button's small bundle does. That's the whole point: interactivity becomes something you add in specific, deliberate places — not something the entire page pays for by default.
One rule that saves debugging time: props passed from a Server Component into a Client Component must be serializable (strings, numbers, plain objects, arrays) — you can't pass a function, a class instance, or a database connection across that boundary, only data.
Share:
Newsletter
Learn what AI is creating for you. Don't get lost.
Get notified when a new deep dive or lesson goes up. No spam, just new posts.
Cooldecode · Lição 07 · Trilha principal
App Router: Server Components vs Client Components
Você já sabe que o App Router traz roteamento por arquivos. O que ele não anuncia tão alto é que todo componente que você escreve dentro dele nasce como Server Component — e esse único padrão muda a forma como você deve construir uma aplicação Next.js.
01Server Components são o padrão, não um extra opcional
No App Router, todo componente é um Server Component, a menos que você diga o contrário. Isso é uma inversão do modelo mental com que a maioria dos devs React cresceu, onde tudo renderiza no navegador e "server-side rendering" era algo adicionado por cima.
Um Server Component roda — como o nome diz — só no servidor. Ele renderiza para HTML (ou um formato especial de streaming) antes de qualquer coisa chegar ao navegador, e por padrão não envia nenhum JavaScript daquele componente para o cliente. Não existe custo de hidratação para ele, porque não há nada para hidratar: ele nunca roda de novo no navegador.
O que um Server Component pode fazer
Ler variáveis de ambiente e segredos diretamente, sem expô-los ao cliente
Consultar um banco de dados ou chamar uma API interna direto dentro do componente, sem precisar de uma rota de API separada
Usar await para buscar dados direto no corpo do componente — sem useEffect, sem gerenciar loading na mão
Importar bibliotecas grandes (um renderizador de markdown, uma lib de datas) sem adicionar um único byte ao bundle do navegador
O que um Server Component não pode fazer
Usar useState, useEffect ou qualquer hook que dependa do componente estar vivo no navegador
Anexar handlers de evento como onClick ou onChange
Tocar em APIs exclusivas do navegador: window, localStorage, geolocalização, etc.
02Client Components: o opt-in explícito
No momento em que um componente precisa de interatividade, state ou uma API de navegador, você marca com uma única linha no topo do arquivo: "use client". Essa diretiva não significa "isso só roda no navegador" — significa "esse componente (e tudo que ele importa) precisa entrar no bundle de JavaScript enviado ao navegador, porque também precisa rodar lá."
Client Components ainda renderizam uma vez no servidor para o HTML inicial (assim a página não fica em branco no primeiro carregamento), e depois hidratam no navegador e se tornam interativos a partir daí — a mesma mecânica que o React sempre usou, só que agora restrita apenas aos componentes que realmente precisam disso.
03A fronteira é a verdadeira decisão de arquitetura
Marcar um arquivo com "use client" não afeta só aquele arquivo — afeta tudo que ele importa. Uma vez que você cruza para o território de client, tudo que vem depois naquela árvore de imports também vai para o navegador. É por isso que o padrão recomendado é empurrar o "use client" o mais para baixo possível na árvore: manter layout, busca de dados e conteúdo estático como Server Components, e isolar a parte interativa — um botão, um formulário, um dropdown — em seu próprio Client Component pequeno.
Aspecto
Server Component
Client Component
Onde roda
Só no servidor
Servidor (primeira renderização) + navegador
JS enviado ao navegador
Nenhum
Sim, o bundle inteiro dele
Hooks (useState, useEffect)
Não permitido
Permitido
Acesso direto a dados (BD, segredos)
Sim
Não — precisa receber via props ou buscar por uma API
APIs de navegador, handlers de evento
Não permitido
Permitido
Padrão no App Router
Sim
Opt-in via "use client"
Mantenha como Server Component
Layouts de página, headers, footers sem interatividade
Qualquer coisa que busca dados para exibir (uma lista, um artigo, as partes estáticas de um dashboard)
Renderização de markdown, formatação, qualquer coisa pesada de CPU mas não interativa
Marque como Client Component
Formulários, botões, toggles, qualquer coisa com onClick/onChange
Componentes usando useState, useEffect ou context
Qualquer coisa que toque window, storage do navegador, ou libs client-only de terceiros
04Exemplo prático: misturando os dois numa página
Uma página de produto pode buscar seus dados no servidor e passar só o necessário para uma peça interativa pequena — por exemplo, um botão "adicionar ao carrinho":
// app/products/[id]/page.jsx — Server Component (padrão, sem diretiva)
import AddToCartButton from "./add-to-cart-button";
export default async function ProductPage({ params }) {
const product = await getProductFromDatabase(params.id);
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={product.id} />
</article>
);
}
A página em si, a busca de dados e o texto do produto nunca chegam ao navegador como JavaScript. Só o bundle pequeno do botão chega. É esse o ponto principal: interatividade passa a ser algo que você adiciona em lugares específicos e deliberados — não algo que a página inteira paga por padrão.
Uma regra que economiza tempo de debug: props passadas de um Server Component para um Client Component precisam ser serializáveis (strings, números, objetos e arrays simples) — você não pode passar uma função, uma instância de classe ou uma conexão de banco através dessa fronteira, só dados.
Compartilhar:
Newsletter
Saiba o que a IA está criando pra você. Não fique por fora.
Seja avisado quando eu postar uma imersão ou lição nova. Sem spam, só posts novos.