← Back to Cooldecode
Cooldecode · Lesson 07 · Main track

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."

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? "Liked" : "Like"}
    </button>
  );
}

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.

AspectServer ComponentClient Component
Where it runsServer onlyServer (first render) + browser
JS sent to browserNoneYes, its whole bundle
Hooks (useState, useEffect)Not allowedAllowed
Direct data access (DB, secrets)YesNo — needs to receive data as props or fetch via an API
Browser APIs, event handlersNot allowedAllowed
Default in the App RouterYesOpt-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>
  );
}
// app/products/[id]/add-to-cart-button.jsx — Client Component
"use client";

import { useState } from "react";

export default function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)}>
      {added ? "Added!" : "Add to cart"}
    </button>
  );
}

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.
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á."

"use client";

import { useState } from "react";

export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? "Curtido" : "Curtir"}
    </button>
  );
}

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.

AspectoServer ComponentClient Component
Onde rodaSó no servidorServidor (primeira renderização) + navegador
JS enviado ao navegadorNenhumSim, o bundle inteiro dele
Hooks (useState, useEffect)Não permitidoPermitido
Acesso direto a dados (BD, segredos)SimNão — precisa receber via props ou buscar por uma API
APIs de navegador, handlers de eventoNão permitidoPermitido
Padrão no App RouterSimOpt-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>
  );
}
// app/products/[id]/add-to-cart-button.jsx — Client Component
"use client";

import { useState } from "react";

export default function AddToCartButton({ productId }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)}>
      {added ? "Adicionado!" : "Adicionar ao carrinho"}
    </button>
  );
}

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.
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.