Apostila Next.js
Do HTML/CSS ao Profissional

JavaScript, TypeScript, React e Next.js do zero — com exercícios, projeto guiado, glossário e dicas de carreira.

HTML/CSS ✔ JavaScript TypeScript React Next.js Deploy 🚀
Sumário

Apostila Next.js — Do HTML/CSS ao Profissional

De páginas estáticas a aplicações web modernas: JavaScript, React e Next.js do zero.


Sobre esta apostila

Esta apostila foi escrita para quem já domina HTML e CSS, mas nunca programou em JavaScript nem usou React ou Next.js. O objetivo é levar você, passo a passo, até o nível de um desenvolvedor front-end capaz de construir e publicar aplicações profissionais com Next.js.

Estrutura:

Convenções usadas:

Pré-requisitos técnicos:

  1. Um computador com Node.js instalado (versão LTS).
  2. O editor Visual Studio Code (gratuito).
  3. Um navegador moderno (Chrome, Firefox, Edge).
  4. Vontade de errar bastante — é assim que se aprende.

PARTE 1 — JAVASCRIPT DO ZERO

Capítulo 1 — Da página estática à web moderna

1.1 O que falta no HTML e CSS?

Você já sabe construir a estrutura (HTML) e a aparência (CSS) de uma página. Mas experimente criar, só com HTML e CSS:

Não dá. Falta o comportamento — e é isso que o JavaScript fornece. Ele é a única linguagem de programação que roda nativamente nos navegadores, e por isso é a base de toda a web moderna.

Analogia: se um site fosse um corpo humano, o HTML seria o esqueleto, o CSS seria a pele e a roupa, e o JavaScript seriam os músculos e o cérebro.

1.2 Onde o JavaScript roda?

  1. No navegador (client-side): manipula a página, reage a cliques, faz animações, busca dados.
  2. No servidor (server-side), via Node.js: gera páginas, acessa bancos de dados, cria APIs.

O Next.js usa JavaScript nos dois lugares ao mesmo tempo — por isso você precisa dominar a linguagem antes de chegar lá.

1.3 Seu primeiro código

Crie um arquivo index.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8" />
  <title>Meu primeiro JavaScript</title>
</head>
<body>
  <h1 id="titulo">Olá!</h1>
  <button id="botao">Clique aqui</button>

  <script>
    // Tudo dentro de <script> é JavaScript
    const botao = document.getElementById("botao");
    botao.addEventListener("click", function () {
      document.getElementById("titulo").textContent = "Você clicou! 🎉";
    });
  </script>
</body>
</html>

Abra no navegador e clique no botão. Você acabou de escrever um programa que reage ao usuário — algo impossível só com HTML/CSS.

💡 Dica: aperte F12 no navegador para abrir as DevTools. A aba Console será sua melhor amiga: nela você vê erros e pode testar código JavaScript em tempo real.

1.4 As três formas de incluir JavaScript

<!-- 1. Inline (evite) -->
<button onclick="alert('oi')">Clique</button>

<!-- 2. Interno -->
<script>
  console.log("Olá do script interno");
</script>

<!-- 3. Externo (a forma profissional) -->
<script src="app.js"></script>

⚠️ Atenção: coloque a tag <script> antes do fechamento do </body> (ou use o atributo defer). Se o script rodar antes de o HTML existir, document.getElementById(...) retorna null e o código quebra.

🧪 Exercícios — Capítulo 1

  1. Crie uma página com um botão que, ao ser clicado, muda a cor de fundo do <body> para uma cor aleatória (pesquise Math.random).
  2. Abra o Console (F12) e digite console.log(2 + 2). Depois digite document.title = "Mudei o título!" e veja o que acontece na aba do navegador.
  3. Explique com suas palavras: qual a diferença entre o papel do CSS e o papel do JavaScript em uma página?

Capítulo 2 — Fundamentos: variáveis, tipos e operadores

2.1 Variáveis: guardando informações

Uma variável é uma "caixinha com etiqueta" onde guardamos um valor para usar depois.

// JavaScript
let idade = 25;          // pode ser reatribuída depois
const nome = "Maria";    // constante: nunca muda
idade = 26;              // ok
// nome = "João";        // ERRO! const não pode ser reatribuída

💡 Dica profissional: use const por padrão e let apenas quando o valor realmente precisar mudar. Nunca use var — é a forma antiga, com comportamentos confusos, mantida só por compatibilidade.

2.2 Tipos de dados primitivos

const texto = "Olá, mundo";      // string (aspas simples, duplas ou crase)
const numero = 42;               // number (inteiros e decimais são o mesmo tipo)
const preco = 19.99;             // number também
const ativo = true;              // boolean (true ou false)
const nada = null;               // ausência intencional de valor
let indefinido;                  // undefined (declarada mas sem valor)

Você pode descobrir o tipo com typeof:

console.log(typeof "abc");   // "string"
console.log(typeof 123);     // "number"
console.log(typeof true);    // "boolean"

2.3 Template literals (crases)

A forma moderna de montar textos com variáveis dentro:

const nome = "Ana";
const idade = 30;

// Forma antiga (concatenação):
const frase1 = "Olá, " + nome + "! Você tem " + idade + " anos.";

// Forma moderna (template literal) — use esta:
const frase2 = `Olá, ${nome}! Você tem ${idade} anos.`;

2.4 Operadores

// Aritméticos
5 + 3;    // 8
5 - 3;    // 2
5 * 3;    // 15
5 / 3;    // 1.666...
5 % 3;    // 2 (resto da divisão)
5 ** 2;   // 25 (potência)

// Comparação — SEMPRE use === e !== (comparação estrita)
5 === 5;      // true
5 === "5";    // false (número não é string)
5 == "5";     // true 😱 (== converte tipos; evite!)
5 !== 3;      // true

// Lógicos
true && false;   // false (E: os dois precisam ser true)
true || false;   // true  (OU: basta um ser true)
!true;           // false (NÃO: inverte)

⚠️ Atenção: = atribui valor, === compara. Escrever if (x = 5) em vez de if (x === 5) é um erro clássico que não gera mensagem de erro — o programa apenas se comporta errado.

2.5 Uma prévia do TypeScript

O TypeScript é o JavaScript com anotações de tipo. O navegador não o entende diretamente — ele é "traduzido" (compilado) para JavaScript antes de rodar. A vantagem: o editor avisa sobre erros antes de você executar o código.

// TypeScript
let idade: number = 25;
const nome: string = "Maria";

idade = "vinte e seis"; // ❌ ERRO detectado no editor:
                        // Type 'string' is not assignable to type 'number'

No mercado de trabalho, a maioria das vagas de React/Next.js pede TypeScript. Nesta apostila você aprende os dois juntos: primeiro a lógica em JavaScript, depois a "camada de segurança" do TypeScript por cima. O Capítulo 8 é dedicado a ele.

🧪 Exercícios — Capítulo 2

  1. Declare variáveis para o seu nome, idade e cidade. Monte uma frase de apresentação com template literal e mostre no console.
  2. No console, teste: "10" + 5, "10" - 5 e 10 === "10". Explique os resultados (pesquise "coerção de tipos" se ficar em dúvida).
  3. Calcule o IMC (peso / altura²) usando variáveis e o operador **. Mostre o resultado com console.log.
  4. Por que const é preferível a let na maioria dos casos?

Capítulo 3 — Controle de fluxo: decisões e repetições

3.1 if / else — tomando decisões

const hora = 14;

if (hora < 12) {
  console.log("Bom dia!");
} else if (hora < 18) {
  console.log("Boa tarde!");
} else {
  console.log("Boa noite!");
}

3.2 Operador ternário — o "if de uma linha"

Muito usado no React, então acostume-se com ele desde já:

const idade = 20;
const status = idade >= 18 ? "maior de idade" : "menor de idade";

Leia assim: "condição ? valor se verdadeiro : valor se falso".

3.3 Valores "truthy" e "falsy"

Em um if, o JavaScript converte qualquer valor para booleano. São falsy (viram false): false, 0, "" (string vazia), null, undefined e NaN. Todo o resto é truthy.

const nomeDigitado = "";

if (nomeDigitado) {
  console.log(`Olá, ${nomeDigitado}`);
} else {
  console.log("Você não digitou nada!"); // cai aqui, pois "" é falsy
}

3.4 switch

Útil quando há muitos valores possíveis para a mesma variável:

const dia = 3;

switch (dia) {
  case 1:
    console.log("Domingo");
    break;
  case 2:
    console.log("Segunda");
    break;
  case 3:
    console.log("Terça");
    break;
  default:
    console.log("Outro dia");
}

⚠️ Sem o break, a execução "vaza" para o próximo case.

3.5 Loops — repetindo tarefas

// for clássico: quando você sabe quantas vezes repetir
for (let i = 1; i <= 5; i++) {
  console.log(`Volta número ${i}`);
}

// while: repete enquanto a condição for verdadeira
let contador = 0;
while (contador < 3) {
  console.log(contador);
  contador++;
}

// for...of: percorre os ITENS de uma lista (o mais usado no dia a dia)
const frutas = ["maçã", "banana", "uva"];
for (const fruta of frutas) {
  console.log(fruta);
}

💡 Dica: no React você quase não usará for — usará o método .map() de arrays (Capítulo 5). Mas entender loops é essencial para a lógica de programação.

🧪 Exercícios — Capítulo 3

  1. Escreva um programa que imprime os números de 1 a 100, mas: para múltiplos de 3 imprime "Fizz", para múltiplos de 5 imprime "Buzz", e para múltiplos de ambos imprime "FizzBuzz". (Este é um teste clássico de entrevista!)
  2. Reescreva este if/else como ternário: if (saldo > 0) { msg = "positivo" } else { msg = "negativo" }.
  3. Dado o array ["HTML", "CSS", "JavaScript"], use for...of para imprimir "Eu sei X" para cada item.
  4. O que imprime if (0) { console.log("A") } else { console.log("B") }? Por quê?

Capítulo 4 — Funções

4.1 O que é uma função?

Uma função é um bloco de código reutilizável com nome. Você define uma vez e chama quantas vezes quiser. É o conceito mais importante da programação — e no React, cada componente da tela será uma função.

// Declaração
function saudacao(nome) {
  return `Olá, ${nome}!`;
}

// Chamada (uso)
const mensagem = saudacao("Carlos");
console.log(mensagem); // "Olá, Carlos!"

4.2 Arrow functions (funções de seta)

A sintaxe moderna, onipresente em código React:

// Função tradicional
function somar(a, b) {
  return a + b;
}

// Arrow function equivalente
const somar = (a, b) => {
  return a + b;
};

// Arrow com "return implícito" (uma expressão só, sem chaves)
const somar = (a, b) => a + b;

// Um parâmetro só? Parênteses opcionais
const dobro = n => n * 2;

4.3 Parâmetros com valor padrão

const saudar = (nome = "visitante") => `Olá, ${nome}!`;

saudar();        // "Olá, visitante!"
saudar("Ana");   // "Olá, Ana!"

4.4 Funções como valores (callbacks)

Em JavaScript, funções são valores como outro qualquer: podem ser guardadas em variáveis e passadas como argumento para outras funções. Uma função passada assim se chama callback.

const executar = (fn) => {
  console.log("Antes");
  fn();               // chama a função recebida
  console.log("Depois");
};

executar(() => console.log("Estou no meio!"));
// Antes → Estou no meio! → Depois

Isso parece abstrato agora, mas é a base de tudo no React: onClick={handleClick} é exatamente isso — passar uma função como valor.

4.5 Versão TypeScript

// TypeScript — tipos nos parâmetros e no retorno
const somar = (a: number, b: number): number => a + b;

const saudar = (nome: string = "visitante"): string => `Olá, ${nome}!`;

somar(2, "3"); // ❌ ERRO: Argument of type 'string' is not assignable to 'number'

🧪 Exercícios — Capítulo 4

  1. Crie uma função calcularDesconto(preco, percentual) que retorna o preço com desconto. Depois converta para arrow function.
  2. Crie uma arrow function ehPar(n) que retorna true se o número for par (dica: operador %).
  3. Crie uma função repetir(vezes, fn) que chama a função fn o número de vezes indicado. Teste com repetir(3, () => console.log("oi")).
  4. Adicione tipos TypeScript às funções dos exercícios 1 e 2 (pode testar no site typescriptlang.org/play).

Capítulo 5 — Arrays e objetos: as estruturas essenciais

5.1 Arrays (listas)

const frutas = ["maçã", "banana", "uva"];

frutas[0];          // "maçã" (índices começam em 0!)
frutas.length;      // 3
frutas.push("kiwi");     // adiciona no final
frutas.includes("uva");  // true

5.2 Os três métodos que você usará TODOS os dias

No desenvolvimento React/Next.js, map, filter e reduce substituem quase todos os loops:

const numeros = [1, 2, 3, 4, 5];

// map: TRANSFORMA cada item, gera um novo array do mesmo tamanho
const dobrados = numeros.map(n => n * 2);
// [2, 4, 6, 8, 10]

// filter: FILTRA itens que passam no teste
const pares = numeros.filter(n => n % 2 === 0);
// [2, 4]

// reduce: REDUZ o array a um único valor
const soma = numeros.reduce((acumulado, n) => acumulado + n, 0);
// 15

// find: encontra o PRIMEIRO item que passa no teste
const primeiroPar = numeros.find(n => n % 2 === 0);
// 2

💡 Dica: o .map() é o método do React. Toda lista renderizada na tela (produtos, posts, comentários) é um array.map() gerando componentes. Domine-o.

5.3 Objetos: dados com nome

Um objeto agrupa dados relacionados em pares chave: valor:

const usuario = {
  nome: "Maria",
  idade: 28,
  email: "maria@email.com",
  ativo: true,
};

// Acesso
usuario.nome;          // "Maria"
usuario["email"];      // "maria@email.com"

// Modificação
usuario.idade = 29;

// Métodos: funções dentro de objetos
const carrinho = {
  itens: [],
  adicionar(item) {
    this.itens.push(item);
  },
};

5.4 Arrays de objetos: o formato dos dados reais

Praticamente todo dado que vem de uma API tem este formato:

const produtos = [
  { id: 1, nome: "Notebook", preco: 3500, estoque: 12 },
  { id: 2, nome: "Mouse",    preco: 80,   estoque: 0 },
  { id: 3, nome: "Teclado",  preco: 150,  estoque: 45 },
];

// Nomes dos produtos disponíveis:
const disponiveis = produtos
  .filter(p => p.estoque > 0)
  .map(p => p.nome);
// ["Notebook", "Teclado"]

// Valor total do estoque:
const total = produtos.reduce((soma, p) => soma + p.preco * p.estoque, 0);

5.5 Destructuring (desestruturação)

Extrair valores de objetos/arrays direto para variáveis — sintaxe usada em todo componente React:

const usuario = { nome: "Ana", idade: 30, cidade: "Lisboa" };

// Sem destructuring
const nome = usuario.nome;
const idade = usuario.idade;

// Com destructuring 🎉
const { nome, idade } = usuario;

// Em arrays (a ordem importa)
const [primeiro, segundo] = ["a", "b", "c"];
// primeiro = "a", segundo = "b"

// Em parâmetros de função (MUITO comum no React)
const apresentar = ({ nome, cidade }) => `${nome} mora em ${cidade}`;
apresentar(usuario); // "Ana mora em Lisboa"

5.6 Spread e imutabilidade

O operador ... (spread) "espalha" os itens de um array/objeto. No React, nunca modificamos dados diretamente — criamos cópias modificadas:

const numeros = [1, 2, 3];
const comQuatro = [...numeros, 4];        // [1, 2, 3, 4] — novo array!

const usuario = { nome: "Ana", idade: 30 };
const atualizado = { ...usuario, idade: 31 }; // novo objeto com idade nova

5.7 Versão TypeScript

// TypeScript
type Produto = {
  id: number;
  nome: string;
  preco: number;
  estoque: number;
};

const produtos: Produto[] = [
  { id: 1, nome: "Notebook", preco: 3500, estoque: 12 },
];

const nomes: string[] = produtos.map(p => p.nome);

Repare: definimos o formato (type Produto) uma vez, e o editor passa a autocompletar p.nome, p.preco etc. e a acusar erros de digitação.

🧪 Exercícios — Capítulo 5

  1. Dado [10, 25, 30, 47, 52], use filter para obter só os maiores que 30 e depois map para dividi-los por 10.
  2. Crie um array de objetos filmes (título, ano, nota). Use filter + map para obter os títulos dos filmes com nota ≥ 8.
  3. Use reduce para calcular a média das notas dos filmes.
  4. Dado const config = { tema: "escuro", idioma: "pt" }, use destructuring para extrair as duas propriedades e spread para criar uma cópia com idioma: "en".
  5. Escreva o type Filme em TypeScript para o exercício 2.

Capítulo 6 — DOM e eventos: JavaScript encontra o HTML

6.1 O que é o DOM?

Quando o navegador lê seu HTML, ele cria uma representação viva da página na memória: o DOM (Document Object Model). O JavaScript pode ler e modificar essa representação — e a página muda na hora.

// Selecionar elementos
const titulo = document.querySelector("h1");        // primeiro <h1>
const botoes = document.querySelectorAll(".btn");   // todos com classe .btn

// Modificar conteúdo e estilo
titulo.textContent = "Novo título";
titulo.style.color = "red";
titulo.classList.add("destaque");
titulo.classList.toggle("escondido");

// Criar elementos
const p = document.createElement("p");
p.textContent = "Parágrafo criado via JS";
document.body.append(p);

💡 querySelector aceita os mesmos seletores do CSS que você já conhece: "#id", ".classe", "ul li a" etc.

6.2 Eventos

const botao = document.querySelector("#enviar");

botao.addEventListener("click", (evento) => {
  console.log("Clicaram em mim!");
});

const campo = document.querySelector("#nome");
campo.addEventListener("input", (e) => {
  console.log("Valor atual:", e.target.value);
});

const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault(); // impede o recarregamento da página
  console.log("Formulário enviado sem recarregar!");
});

Eventos comuns: click, input, change, submit, keydown, mouseover, scroll.

6.3 Um mini-app completo (sem React)

Lista de tarefas em JavaScript puro — guarde este exemplo, pois no Capítulo 11 refaremos em React e você verá a diferença:

<input id="tarefa" placeholder="Nova tarefa" />
<button id="add">Adicionar</button>
<ul id="lista"></ul>

<script>
  const input = document.querySelector("#tarefa");
  const botao = document.querySelector("#add");
  const lista = document.querySelector("#lista");

  botao.addEventListener("click", () => {
    if (!input.value) return;          // ignora vazio
    const li = document.createElement("li");
    li.textContent = input.value;
    li.addEventListener("click", () => li.remove()); // clicar remove
    lista.append(li);
    input.value = "";
  });
</script>

6.4 Por que o React existe, então?

Repare no exemplo acima: para cada mudança, você precisa selecionar elementos, criar nós, atualizar textos, remover filhos... Em apps grandes isso vira um caos inadministrável. O React inverte a lógica: você descreve como a tela deve ficar para cada estado dos dados, e ele atualiza o DOM sozinho. Guarde essa frase — ela é a essência da Parte 2.

🧪 Exercícios — Capítulo 6

  1. Crie um contador: um número na tela, um botão "+" e um botão "−".
  2. Crie um campo de texto que mostra, em tempo real (input), quantos caracteres foram digitados.
  3. Melhore o app de tarefas: adicione um botão "Limpar tudo" e faça Enter no campo também adicionar a tarefa (evento keydown, tecla "Enter").
  4. Crie um botão "modo escuro" que alterna uma classe dark no <body> (use classList.toggle e estilize a classe no CSS).

Capítulo 7 — JavaScript moderno: módulos, assincronia e APIs

7.1 Módulos: dividindo o código em arquivos

Projetos reais têm centenas de arquivos. Os módulos ES permitem exportar de um arquivo e importar em outro:

// arquivo: matematica.js
export const somar = (a, b) => a + b;
export const PI = 3.14159;
export default function multiplicar(a, b) {
  return a * b;
}
// arquivo: app.js
import multiplicar, { somar, PI } from "./matematica.js";

console.log(somar(2, 3));       // 5
console.log(multiplicar(4, 5)); // 20

No React/Next.js, cada componente vive em seu próprio arquivo e usa exatamente essa sintaxe.

7.2 Código assíncrono: o problema

Buscar dados de um servidor demora (milissegundos ou segundos). O JavaScript não para esperando — ele continua executando o resto e volta quando a resposta chega. Isso se chama assincronia.

7.3 Promises

Uma Promise ("promessa") representa um valor que chegará no futuro. Ela pode ser: pendente → cumprida (com um valor) ou rejeitada (com um erro).

fetch("https://api.exemplo.com/dados")   // retorna uma Promise
  .then(resposta => resposta.json())     // quando chegar, converte
  .then(dados => console.log(dados))     // quando converter, usa
  .catch(erro => console.error(erro));   // se algo falhar

7.4 async/await: a forma moderna e legível

async/await é "açúcar sintático" sobre Promises — mesmo mecanismo, leitura muito mais natural:

// JavaScript
async function carregarUsuario() {
  try {
    const resposta = await fetch("https://jsonplaceholder.typicode.com/users/1");
    if (!resposta.ok) throw new Error(`Erro HTTP: ${resposta.status}`);
    const usuario = await resposta.json();
    console.log(usuario.name);
  } catch (erro) {
    console.error("Falha ao carregar:", erro.message);
  }
}

carregarUsuario();

Regras:

💡 Isto é fundamental para o Next.js: componentes de servidor são funções async que fazem await fetch(...) diretamente. Você usará isso o tempo todo.

7.5 JSON: o idioma das APIs

JSON (JavaScript Object Notation) é o formato de texto usado para trocar dados entre sistemas. Parece um objeto JavaScript, com regras mais rígidas (chaves entre aspas duplas):

{
  "id": 1,
  "nome": "Maria",
  "habilidades": ["HTML", "CSS", "JavaScript"]
}
const texto = '{"nome": "Maria"}';
const obj = JSON.parse(texto);        // texto → objeto
const volta = JSON.stringify(obj);    // objeto → texto

7.6 Versão TypeScript

// TypeScript
type Usuario = {
  id: number;
  name: string;
  email: string;
};

async function carregarUsuario(): Promise<Usuario> {
  const resposta = await fetch("https://jsonplaceholder.typicode.com/users/1");
  if (!resposta.ok) throw new Error(`Erro HTTP: ${resposta.status}`);
  return resposta.json() as Promise<Usuario>;
}

🧪 Exercícios — Capítulo 7

  1. Crie dois arquivos: util.js (exporta capitalizar(texto) que deixa a primeira letra maiúscula) e main.js (importa e usa). Rode com node main.js (adicione "type": "module" no package.json ou use extensão .mjs).
  2. Use fetch + async/await para buscar https://jsonplaceholder.typicode.com/posts/1 e mostrar o título no console.
  3. Modifique o exercício 2 para buscar /posts (lista completa) e imprimir apenas os títulos dos 5 primeiros (dica: .slice(0, 5) + .map).
  4. O que acontece se você esquecer o await antes do fetch? Teste e explique.

Capítulo 8 — TypeScript na prática

8.1 Por que o mercado adotou TypeScript?

Em projetos grandes, a maioria dos bugs de JavaScript é banal: passar string onde era número, errar o nome de uma propriedade, esquecer que algo pode ser null. O TypeScript elimina essa classe inteira de erros antes de o código rodar, além de dar autocompletar preciso no editor. Resultado: praticamente todas as vagas React/Next.js hoje pedem TS.

A boa notícia: todo JavaScript válido é TypeScript válido. Você adiciona tipos gradualmente.

8.2 Tipos básicos

let nome: string = "Ana";
let idade: number = 30;
let ativo: boolean = true;
let tags: string[] = ["dev", "front"];
let qualquerCoisa: any = "evite o any!"; // desliga a checagem — use com moderação

Na prática, o TS infere tipos sozinho — você não precisa anotar tudo:

let cidade = "Lisboa";   // TS já sabe que é string
cidade = 42;             // ❌ erro, mesmo sem anotação

8.3 type e interface: descrevendo objetos

type Usuario = {
  id: number;
  nome: string;
  email: string;
  telefone?: string;          // "?" = propriedade opcional
  readonly criadoEm: Date;    // não pode ser alterada depois
};

const u: Usuario = {
  id: 1,
  nome: "Maria",
  email: "m@email.com",
  criadoEm: new Date(),
};

interface faz quase o mesmo que type para objetos. Convenção comum: interface para formatos de objetos/props, type para uniões e o resto. Não trave nisso — os times escolhem um padrão e seguem.

8.4 Union types e literais

type Status = "pendente" | "aprovado" | "rejeitado";

let pedido: Status = "pendente";
pedido = "cancelado"; // ❌ erro: não está na lista

// Union de tipos diferentes
let id: number | string = 123;
id = "abc-123"; // ok

Isso é poderosíssimo: o editor autocompleta os valores possíveis e impede typos.

8.5 Funções tipadas e generics (noção básica)

// Funções
function calcularTotal(precos: number[], desconto: number = 0): number {
  const soma = precos.reduce((acc, p) => acc + p, 0);
  return soma * (1 - desconto);
}

// Generics: tipos "parametrizados" — a função funciona para qualquer tipo T
function primeiro<T>(lista: T[]): T | undefined {
  return lista[0];
}

primeiro([1, 2, 3]);         // TS sabe que retorna number | undefined
primeiro(["a", "b"]);        // TS sabe que retorna string | undefined

Você não precisa criar generics no início da carreira, mas precisa ler: useState<string>, Promise<Usuario>, Array<number> são generics.

8.6 null, undefined e o operador ?.

type Perfil = { bio?: string };

const p: Perfil = {};

console.log(p.bio.length);    // ❌ TS avisa: 'p.bio' is possibly 'undefined'
console.log(p.bio?.length);   // ✅ optional chaining: undefined se bio não existir
console.log(p.bio ?? "Sem bio"); // ✅ nullish coalescing: valor padrão

8.7 Como rodar TypeScript?

No dia a dia com Next.js, você não configura nada: o framework já vem com TS integrado — basta usar arquivos .ts/.tsx. Para experimentar isoladamente, use o TypeScript Playground no navegador.

🧪 Exercícios — Capítulo 8

  1. Crie um type Livro com título, autor, ano e páginas (opcional). Crie um array Livro[] com 3 livros.
  2. Crie type Prioridade = "baixa" | "media" | "alta" e uma função corDaPrioridade(p: Prioridade): string que retorna uma cor para cada valor (use switch).
  3. Tipifique a função: const aplicarDesconto = (preco, cupom) => cupom === "DEZ" ? preco * 0.9 : preco.
  4. Explique a diferença entre p.bio.length, p.bio?.length e p.bio ?? "padrão".

PARTE 2 — REACT

Capítulo 9 — O que é React e por que ele domina o mercado

9.1 O problema que o React resolve

Relembre o app de tarefas do Capítulo 6: cada mudança nos dados exigia manipular o DOM manualmente. Agora imagine o feed do Instagram: curtidas, comentários, stories, notificações — milhares de elementos mudando ao mesmo tempo. Manter o DOM sincronizado "na mão" é inviável.

O React (criado pelo Facebook em 2013) propõe outra abordagem:

Você descreve como a interface deve ficar para cada estado dos dados. Quando os dados mudam, o React atualiza o DOM sozinho — e só o necessário.

É a chamada programação declarativa (descrever o quê) em vez de imperativa (comandar como, passo a passo).

9.2 Componentes: peças de LEGO

No React, a interface é dividida em componentes: funções que retornam pedaços de tela. Um componente pode conter outros:

<App>
 ├── <Cabecalho>
 │     ├── <Logo />
 │     └── <Menu />
 ├── <ListaDeProdutos>
 │     ├── <CartaoProduto />
 │     ├── <CartaoProduto />
 │     └── <CartaoProduto />
 └── <Rodape />

Cada peça é reutilizável, isolada e testável. É assim que times grandes constroem apps gigantes sem enlouquecer.

9.3 JSX: HTML dentro do JavaScript

Um componente React é uma função que retorna JSX — uma sintaxe que parece HTML, mas vive dentro do JavaScript:

function BemVindo() {
  const nome = "Maria";
  return (
    <div className="cartao">
      <h1>Olá, {nome}!</h1>
      <p>Hoje é {new Date().toLocaleDateString("pt-BR")}</p>
    </div>
  );
}

Diferenças do JSX para o HTML que você conhece:

HTML JSX Motivo
class="x" className="x" class é palavra reservada do JS
for="x" htmlFor="x" for é palavra reservada do JS
onclick="..." onClick={fn} camelCase + recebe função, não string
style="color:red" style={{ color: "red" }} objeto JS, não string
<img> <img /> toda tag deve fechar

Regras de ouro do JSX:

  1. Chaves {} inserem JavaScript no meio do "HTML": variáveis, expressões, ternários, .map().
  2. O componente deve retornar um único elemento raiz. Se precisar de vários irmãos, envolva com um Fragment: <>...</>.
  3. Nomes de componentes começam com letra maiúscula (<CartaoProduto />); minúscula é reservada para tags HTML.
// Fragment: agrupa sem criar div extra no DOM
function Info() {
  return (
    <>
      <h2>Título</h2>
      <p>Parágrafo</p>
    </>
  );
}

9.4 React x Next.js: qual a relação?

Analogia: o React é o motor; o Next.js é o carro completo. Hoje, a própria documentação oficial do React recomenda começar projetos com um framework como o Next.js. Nos capítulos 9–15 aprenderemos o "motor" usando exemplos simples; a partir do 16, montamos o carro.

💡 Para praticar React isoladamente sem instalar nada, use os playgrounds online CodeSandbox ou StackBlitz (escolha o template "React" ou "React TypeScript").

🧪 Exercícios — Capítulo 9

  1. Explique com suas palavras a diferença entre programação imperativa e declarativa. Use o app de tarefas do Capítulo 6 como exemplo.
  2. Olhe a página inicial de um site que você usa (ex.: YouTube) e desenhe no papel a árvore de componentes que você criaria.
  3. Converta este HTML para JSX válido: <div class="perfil"><img src="foto.png"><label for="nome">Nome</label></div>.
  4. No CodeSandbox, crie um componente Perfil que mostra seu nome em um <h1> e sua cidade em um <p>, usando variáveis com {}.

Capítulo 10 — Props: componentes que recebem dados

10.1 O conceito

Props (de properties) são os "atributos" que um componente recebe — como os atributos de uma tag HTML, mas com qualquer valor JavaScript: textos, números, arrays, objetos, até funções.

// Definindo o componente (recebe props)
function CartaoProduto(props) {
  return (
    <div className="cartao">
      <h2>{props.nome}</h2>
      <p>R$ {props.preco}</p>
    </div>
  );
}

// Usando o componente (passa props)
function App() {
  return (
    <main>
      <CartaoProduto nome="Notebook" preco={3500} />
      <CartaoProduto nome="Mouse" preco={80} />
    </main>
  );
}

Mesmo componente, dados diferentes → telas diferentes. Reutilização é o superpoder das props.

10.2 Destructuring nas props (o padrão do mercado)

Lembra do destructuring do Capítulo 5? Ele brilha aqui:

// Em vez de props.nome, props.preco...
function CartaoProduto({ nome, preco, emPromocao = false }) {
  return (
    <div className="cartao">
      <h2>{nome}</h2>
      <p>R$ {preco}</p>
      {emPromocao && <span className="badge">PROMOÇÃO 🔥</span>}
    </div>
  );
}

Repare também no padrão {condicao && <Elemento />}: renderiza o elemento somente se a condição for verdadeira (renderização condicional — mais no Capítulo 12).

10.3 A prop especial children

Tudo que você coloca entre as tags de um componente chega como props.children:

function Painel({ titulo, children }) {
  return (
    <section className="painel">
      <h2>{titulo}</h2>
      <div className="conteudo">{children}</div>
    </section>
  );
}

// Uso:
<Painel titulo="Avisos">
  <p>Este conteúdo vira o children!</p>
  <button>Ok</button>
</Painel>

É assim que se criam "molduras" reutilizáveis: cards, modais, layouts.

10.4 Props são somente leitura

⚠️ Regra inviolável: um componente nunca modifica suas próprias props. Dados fluem de cima para baixo (pai → filho). Se algo precisa mudar, isso é estado — assunto do próximo capítulo.

10.5 Versão TypeScript

// TypeScript (.tsx)
type CartaoProdutoProps = {
  nome: string;
  preco: number;
  emPromocao?: boolean;               // opcional
};

function CartaoProduto({ nome, preco, emPromocao = false }: CartaoProdutoProps) {
  return (
    <div className="cartao">
      <h2>{nome}</h2>
      <p>R$ {preco.toFixed(2)}</p>
      {emPromocao && <span>PROMOÇÃO 🔥</span>}
    </div>
  );
}

// Com children:
type PainelProps = {
  titulo: string;
  children: React.ReactNode;   // tipo oficial para "qualquer conteúdo JSX"
};

Se alguém escrever <CartaoProduto nome="X" preco="caro" />, o editor acusa o erro na hora. É por isso que o mercado ama TS + React.

🧪 Exercícios — Capítulo 10

  1. Crie um componente Avatar que recebe src e nome e renderiza <img> com alt correto.
  2. Crie um componente Botao que recebe children e uma prop variante ("primario" ou "secundario") e aplica classes CSS diferentes para cada uma.
  3. Crie PerfilUsuario que recebe um objeto usuario (nome, cargo, foto) e o exibe usando o Avatar do exercício 1 dentro dele (composição!).
  4. Tipifique os três componentes acima com TypeScript.

Capítulo 11 — Estado: componentes que reagem (useState)

11.1 O que é estado?

Estado (state) é a memória interna de um componente: dados que mudam com o tempo e que, ao mudar, devem redesenhar a tela. Contador de curtidas, texto digitado, item selecionado, modal aberto/fechado — tudo isso é estado.

11.2 O hook useState

import { useState } from "react";

function Contador() {
  const [contagem, setContagem] = useState(0);
  //     ↑ valor      ↑ função p/ mudar   ↑ valor inicial

  return (
    <div>
      <p>Você clicou {contagem} vezes</p>
      <button onClick={() => setContagem(contagem + 1)}>+1</button>
    </div>
  );
}

O fluxo mágico do React em 3 passos:

  1. Usuário clica → chamamos setContagem(novoValor).
  2. O React re-executa a função do componente com o novo valor.
  3. O React compara o JSX novo com o antigo e atualiza só o que mudou no DOM.

Você nunca mais escreve document.querySelector. Você muda o dado; a tela acompanha.

⚠️ Nunca modifique o estado diretamente (contagem++ não redesenha nada). Sempre use a função set....

11.3 Eventos no React

function Formulario() {
  const [nome, setNome] = useState("");

  const aoEnviar = (e) => {
    e.preventDefault();
    alert(`Olá, ${nome}!`);
  };

  return (
    <form onSubmit={aoEnviar}>
      <input
        value={nome}
        onChange={(e) => setNome(e.target.value)}
        placeholder="Seu nome"
      />
      <button>Enviar</button>
      <p>Prévia: {nome}</p>
    </form>
  );
}

Este padrão — value vem do estado, onChange atualiza o estado — chama-se componente controlado e é a forma padrão de lidar com inputs no React.

11.4 Estado com arrays e objetos: imutabilidade

Lembra do spread (...) do Capítulo 5? Aqui está o porquê dele:

function ListaDeTarefas() {
  const [tarefas, setTarefas] = useState(["Estudar React"]);
  const [texto, setTexto] = useState("");

  const adicionar = () => {
    if (!texto) return;
    setTarefas([...tarefas, texto]);   // NOVO array (nunca tarefas.push!)
    setTexto("");
  };

  const remover = (indice) => {
    setTarefas(tarefas.filter((_, i) => i !== indice)); // NOVO array
  };

  return (
    <div>
      <input value={texto} onChange={(e) => setTexto(e.target.value)} />
      <button onClick={adicionar}>Adicionar</button>
      <ul>
        {tarefas.map((t, i) => (
          <li key={i} onClick={() => remover(i)}>{t}</li>
        ))}
      </ul>
    </div>
  );
}

Compare com a versão em JavaScript puro do Capítulo 6: aqui não há uma linha de manipulação de DOM. Descrevemos a tela; o React faz o resto.

11.5 Subindo o estado (lifting state up)

Quando dois componentes precisam do mesmo dado, o estado sobe para o pai comum, que o distribui via props:

function App() {
  const [tema, setTema] = useState("claro");
  return (
    <>
      <BotaoTema tema={tema} aoTrocar={() => setTema(tema === "claro" ? "escuro" : "claro")} />
      <Painel tema={tema} />
    </>
  );
}

Repare: o filho recebe uma função via props (aoTrocar) e a chama para "avisar" o pai. Dados descem, eventos sobem.

11.6 Versão TypeScript

const [contagem, setContagem] = useState<number>(0);   // geralmente inferido
const [nome, setNome] = useState("");                  // infere string
const [tarefas, setTarefas] = useState<string[]>([]);  // necessário: array vazio não infere

type Usuario = { id: number; nome: string };
const [usuario, setUsuario] = useState<Usuario | null>(null); // padrão comum: "ainda não carregou"

🧪 Exercícios — Capítulo 11

  1. Crie um contador com botões +, e Zerar, que nunca fica negativo.
  2. Crie um campo de texto com contagem de caracteres e limite de 100 (desabilite o botão "Enviar" acima disso — atributo disabled).
  3. Refaça a lista de tarefas com objetos { id, texto, concluida }: clicar alterna concluida (risque com CSS) e um botão ✕ remove.
  4. Crie dois componentes irmãos: um <input> e um <h1> que mostra o texto digitado em tempo real. O estado deve morar no pai.

Capítulo 12 — Listas e renderização condicional

12.1 Renderizando listas com .map()

O padrão mais usado de todo o React — dados viram componentes:

const produtos = [
  { id: 1, nome: "Notebook", preco: 3500 },
  { id: 2, nome: "Mouse", preco: 80 },
  { id: 3, nome: "Teclado", preco: 150 },
];

function Loja() {
  return (
    <ul>
      {produtos.map((p) => (
        <li key={p.id}>
          {p.nome}  R$ {p.preco}
        </li>
      ))}
    </ul>
  );
}

12.2 A prop key

⚠️ Todo item gerado por .map() precisa de uma prop key única e estável (geralmente o id do dado). É como o React identifica cada item para atualizar a lista com eficiência quando algo muda de posição, entra ou sai.

12.3 As três formas de renderização condicional

function Painel({ usuario, carregando, erros }) {
  // 1. if/return antecipado — para casos "ou isso, ou aquilo"
  if (carregando) return <p>Carregando...</p>;

  return (
    <div>
      {/* 2. Ternário — escolhe entre dois JSX */}
      {usuario ? <h1>Olá, {usuario.nome}</h1> : <a href="/login">Entrar</a>}

      {/* 3. && — mostra ou não mostra */}
      {erros.length > 0 && <p className="erro">{erros.length} erro(s) encontrado(s)</p>}
    </div>
  );
}

⚠️ Pegadinha do &&: {itens.length && <Lista />} renderiza o número 0 na tela quando a lista está vazia! Use comparação explícita: {itens.length > 0 && <Lista />}.

12.4 Exemplo completo: filtro + lista + estado vazio

// TypeScript
type Produto = { id: number; nome: string; preco: number };

function Loja({ produtos }: { produtos: Produto[] }) {
  const [busca, setBusca] = useState("");

  const filtrados = produtos.filter((p) =>
    p.nome.toLowerCase().includes(busca.toLowerCase())
  );

  return (
    <div>
      <input
        value={busca}
        onChange={(e) => setBusca(e.target.value)}
        placeholder="Buscar produto..."
      />

      {filtrados.length === 0 ? (
        <p>Nenhum produto encontrado 😕</p>
      ) : (
        <ul>
          {filtrados.map((p) => (
            <li key={p.id}>{p.nome}  R$ {p.preco}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Repare no padrão profissional: derivamos filtrados do estado durante o render, em vez de criar um segundo estado para a lista filtrada. Regra: se dá para calcular a partir do que já existe, calcule — não duplique estado.

🧪 Exercícios — Capítulo 12

  1. Renderize uma lista de contatos (nome, telefone) a partir de um array de objetos, com key correta.
  2. Adicione à Loja acima um <select> para ordenar por preço (crescente/decrescente). Dica: [...filtrados].sort(...).
  3. Crie um componente Semaforo com estado "verde" | "amarelo" | "vermelho" e um botão "Avançar" que troca a cor exibida (ternário aninhado ou objeto de mapeamento).
  4. Reproduza a pegadinha do && com uma lista vazia e depois corrija-a.

Capítulo 13 — Efeitos: useEffect e o mundo exterior

13.1 O que é um "efeito"?

Renderizar JSX é a função principal do componente. Tudo que acontece fora disso — buscar dados de uma API, iniciar um timer, mudar o título da aba, salvar no localStorage — é um efeito colateral (side effect). O hook useEffect é o lugar oficial para eles.

import { useEffect, useState } from "react";

function Relogio() {
  const [hora, setHora] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setHora(new Date()), 1000);
    return () => clearInterval(id);   // limpeza: roda ao desmontar
  }, []);                             // [] = roda 1 vez, após o 1º render

  return <p>{hora.toLocaleTimeString("pt-BR")}</p>;
}

13.2 O array de dependências

O segundo argumento do useEffect controla quando ele roda:

Dependências Quando roda
(omitido) Após todo render (raramente o que você quer)
[] Uma vez, após o primeiro render
[busca, pagina] No primeiro render e sempre que busca ou pagina mudarem

13.3 Buscando dados de uma API (o uso clássico)

// TypeScript
type Post = { id: number; title: string };

function Posts() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState<string | null>(null);

  useEffect(() => {
    async function carregar() {
      try {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts");
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        setPosts(await res.json());
      } catch (e) {
        setErro(e instanceof Error ? e.message : "Erro desconhecido");
      } finally {
        setCarregando(false);
      }
    }
    carregar();
  }, []);

  if (carregando) return <p>Carregando...</p>;
  if (erro) return <p>Erro: {erro}</p>;

  return (
    <ul>
      {posts.slice(0, 10).map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

Memorize o trio carregando / erro / dados — é o esqueleto de 90% das telas que consomem APIs.

💡 Spoiler importante: no Next.js, esse padrão inteiro muitas vezes desaparece — Server Components buscam dados com um simples await fetch() antes de a página chegar ao navegador (Capítulo 20). Mas o useEffect continua necessário para dados que dependem de interação do usuário.

13.4 A função de limpeza (cleanup)

Se o efeito "liga" algo (timer, listener, conexão), o return deve "desligar", evitando vazamentos de memória:

useEffect(() => {
  const aoRedimensionar = () => console.log(window.innerWidth);
  window.addEventListener("resize", aoRedimensionar);
  return () => window.removeEventListener("resize", aoRedimensionar);
}, []);

13.5 Erros clássicos com useEffect

  1. Esquecer o array de dependências → efeito roda em todo render → loops infinitos se ele alterar estado.
  2. Mentir nas dependências → usar busca dentro do efeito mas não listá-la → dados desatualizados. O ESLint do React avisa; obedeça a ele.
  3. Usar useEffect para tudo → cálculos derivados de estado/props não precisam de efeito; calcule direto no render (como filtrados no Capítulo 12).

🧪 Exercícios — Capítulo 13

  1. Crie um componente que muda document.title para refletir o valor de um contador (efeito com dependência).
  2. Crie um cronômetro com Iniciar/Pausar/Zerar usando setInterval + limpeza correta.
  3. Busque https://jsonplaceholder.typicode.com/users e mostre os nomes, com estados de carregamento e erro.
  4. Adicione um <input> de busca ao exercício 3 que filtra os usuários sem novo fetch. A filtragem precisa de useEffect? Por quê?

Capítulo 14 — Formulários e hooks adicionais

14.1 Formulário completo (componentes controlados)

// TypeScript
type DadosForm = { nome: string; email: string; assunto: string };

function Contato() {
  const [dados, setDados] = useState<DadosForm>({ nome: "", email: "", assunto: "duvida" });
  const [enviado, setEnviado] = useState(false);

  // Um handler genérico para todos os campos:
  const aoMudar = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    setDados({ ...dados, [e.target.name]: e.target.value });
  };

  const aoEnviar = (e: React.FormEvent) => {
    e.preventDefault();
    if (!dados.email.includes("@")) return alert("E-mail inválido");
    console.log("Enviando:", dados);
    setEnviado(true);
  };

  if (enviado) return <p>Obrigado, {dados.nome}! </p>;

  return (
    <form onSubmit={aoEnviar}>
      <input name="nome" value={dados.nome} onChange={aoMudar} placeholder="Nome" required />
      <input name="email" value={dados.email} onChange={aoMudar} placeholder="E-mail" type="email" />
      <select name="assunto" value={dados.assunto} onChange={aoMudar}>
        <option value="duvida">Dúvida</option>
        <option value="orcamento">Orçamento</option>
      </select>
      <button>Enviar</button>
    </form>
  );
}

Repare no truque [e.target.name]: e.target.value — um único handler serve para todos os campos, graças ao atributo name.

💡 Em projetos grandes, bibliotecas como React Hook Form + Zod (validação) são o padrão de mercado. Aprenda o modo manual primeiro; as bibliotecas farão mais sentido depois.

14.2 useRef: referências sem re-render

useRef guarda um valor que não dispara re-render ao mudar. Uso mais comum: acessar um elemento do DOM diretamente (focar um input, por exemplo):

import { useRef, useEffect } from "react";

function Busca() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();   // foca ao montar
  }, []);

  return <input ref={inputRef} placeholder="Buscar..." />;
}

14.3 useContext: dados globais sem "prop drilling"

Quando um dado (tema, usuário logado, idioma) precisa estar disponível em dezenas de componentes, passá-lo por props nível a nível ("prop drilling") vira tortura. O Context cria um "canal" direto:

import { createContext, useContext, useState } from "react";

const TemaContext = createContext("claro");

function App() {
  const [tema, setTema] = useState("claro");
  return (
    <TemaContext.Provider value={tema}>
      <button onClick={() => setTema(t => t === "claro" ? "escuro" : "claro")}>Trocar</button>
      <PaginaProfunda />
    </TemaContext.Provider>
  );
}

function BotaoLaDentro() {           // pode estar a 10 níveis de profundidade
  const tema = useContext(TemaContext);
  return <button className={tema}>Tema atual: {tema}</button>;
}

14.4 Hooks customizados: sua lógica reutilizável

Qualquer função que começa com use e usa hooks dentro é um hook customizado — a forma de compartilhar lógica entre componentes:

// useLocalStorage.js — estado que persiste no navegador
import { useState, useEffect } from "react";

export function useLocalStorage(chave, valorInicial) {
  const [valor, setValor] = useState(() => {
    const salvo = localStorage.getItem(chave);
    return salvo !== null ? JSON.parse(salvo) : valorInicial;
  });

  useEffect(() => {
    localStorage.setItem(chave, JSON.stringify(valor));
  }, [chave, valor]);

  return [valor, setValor];
}

// Uso em qualquer componente — igualzinho ao useState:
const [tema, setTema] = useLocalStorage("tema", "claro");

14.5 As regras dos hooks

  1. Só chame hooks no topo do componente — nunca dentro de if, loops ou funções aninhadas.
  2. Só chame hooks em componentes React ou em outros hooks.

O ESLint (já configurado no Next.js) fiscaliza essas regras para você.

🧪 Exercícios — Capítulo 14

  1. Crie um formulário de cadastro (nome, e-mail, senha, confirmar senha) com validação: senhas iguais e mínimo de 8 caracteres, exibindo mensagens de erro por campo.
  2. Use useRef para criar um botão "Ir para o formulário" que faz scroll até ele (ref.current.scrollIntoView()).
  3. Crie um hook useContadorRegressivo(segundos) que retorna o tempo restante e uma função reiniciar.
  4. Crie um Context de "usuário logado" com um botão Entrar/Sair no topo e um componente distante que mostra o nome do usuário.

Capítulo 15 — Pensando em React: arquitetura de componentes

15.1 O processo profissional em 5 passos

Diante de um layout novo, o profissional segue este roteiro (baseado no guia oficial "Thinking in React"):

  1. Quebre o layout em componentes — desenhe retângulos sobre o mockup e nomeie cada um.
  2. Construa a versão estática — só props, sem estado. Dados fake, tela pronta.
  3. Identifique o estado mínimo — o que muda com o tempo e não pode ser derivado? (Lista filtrada NÃO é estado; o texto da busca É.)
  4. Decida onde o estado mora — no pai comum mais próximo de todos que o usam.
  5. Adicione o fluxo inverso — filhos avisam o pai por funções recebidas via props.

15.2 Checklist do que é (e não é) estado

Para cada dado, pergunte:

15.3 Componentes de apresentação x componentes de lógica

Um padrão mental útil: alguns componentes só mostram (recebem props, devolvem JSX bonito), outros orquestram (têm estado, efeitos, e distribuem dados). Manter essa separação deixa o código testável e reutilizável.

<PaginaProdutos>        ← lógica: busca dados, guarda filtros (estado)
  <BarraDeBusca />      ← apresentação: input + callback
  <GradeDeProdutos>     ← apresentação: recebe lista, faz .map()
    <CartaoProduto />   ← apresentação pura

15.4 Onde estudar mais React

🧪 Exercícios — Capítulo 15

  1. Pegue o mockup de um card de rede social (avatar, nome, texto, botões curtir/comentar) e aplique os 5 passos: liste os componentes, as props e o estado mínimo.
  2. No app de tarefas do Capítulo 11, o array tarefas é estado. E o número de tarefas concluídas — deve ser estado? Justifique.
  3. Construa a "Loja" completa: busca + filtro de categoria + grade de produtos + contador de resultados, aplicando o processo dos 5 passos. Este é seu exame final de React!

PARTE 3 — NEXT.JS

Capítulo 16 — O que é Next.js e por que ele existe

16.1 As limitações do React "puro"

Um app React tradicional (SPA — Single Page Application) envia ao navegador um HTML quase vazio + um pacotão de JavaScript que constrói toda a tela no cliente. Problemas:

  1. SEO fraco: robôs de busca podem receber uma página vazia.
  2. Primeira carga lenta: o usuário espera o JS baixar e executar para ver qualquer coisa.
  3. Faltam peças: rotas, otimização de imagem, API, configuração de build... tudo é por sua conta.

16.2 A proposta do Next.js

O Next.js (criado pela Vercel) resolve isso renderizando as páginas no servidor e entregando HTML pronto — e ainda embala tudo que um projeto profissional precisa:

Recurso O que resolve
Roteamento por arquivos Criar uma pasta = criar uma rota. Sem configuração.
SSR / SSG / ISR HTML gerado no servidor: SEO e velocidade (Cap. 20)
Server Components Buscar dados direto no componente, sem API intermediária
next/image, next/font Imagens e fontes otimizadas automaticamente
Route Handlers / Server Actions Backend (API) dentro do mesmo projeto
TypeScript, ESLint, bundler Já configurados no create-next-app

16.3 Criando seu primeiro projeto

No terminal (precisa do Node.js instalado):

npx create-next-app@latest meu-site

O instalador fará perguntas. Sugestão para acompanhar a apostila:

Would you like to use TypeScript?        → Yes
Would you like to use ESLint?            → Yes
Would you like to use Tailwind CSS?      → Yes
Would you like your code inside a src/?  → No
Would you like to use App Router?        → Yes  (essencial!)
Would you like to use Turbopack?         → Yes
Import alias?                            → No (padrão @/*)

Depois:

cd meu-site
npm run dev

Abra http://localhost:3000 — seu site está no ar (localmente). Edite app/page.tsx, salve, e veja a página atualizar sozinha (Fast Refresh).

16.4 Anatomia do projeto

meu-site/
├── app/                  ← suas páginas e layouts (o coração)
│   ├── layout.tsx        ← layout raiz (html, body, cabeçalho...)
│   ├── page.tsx          ← página inicial (rota /)
│   └── globals.css       ← CSS global
├── public/               ← arquivos estáticos (imagens, favicon...)
├── node_modules/         ← dependências instaladas (não mexa)
├── package.json          ← nome, scripts e dependências do projeto
├── tsconfig.json         ← configuração do TypeScript
└── next.config.ts        ← configuração do Next.js

Scripts do package.json:

💡 Esta apostila usa o App Router (pasta app/), o padrão desde o Next.js 13. Você ainda verá tutoriais antigos com o Pages Router (pasta pages/) — funciona, mas não é o caminho para projetos novos.

🧪 Exercícios — Capítulo 16

  1. Instale o Node.js, crie o projeto meu-site e rode npm run dev.
  2. Edite app/page.tsx: apague o conteúdo do return e coloque um <h1> com seu nome. Veja o Fast Refresh agir.
  3. Rode npm run build e leia o resumo no terminal: quais rotas foram geradas? O que significa o símbolo (Static)?
  4. Explique com suas palavras: qual a diferença entre biblioteca (React) e framework (Next.js)?

Capítulo 17 — Rotas, páginas e layouts (App Router)

17.1 Pastas viram rotas

No App Router, a estrutura de pastas dentro de app/ define as URLs. O arquivo page.tsx torna a rota acessível:

app/
├── page.tsx               →  /
├── sobre/
│   └── page.tsx           →  /sobre
├── blog/
│   ├── page.tsx           →  /blog
│   └── [slug]/
│       └── page.tsx       →  /blog/qualquer-coisa  (rota dinâmica!)
└── dashboard/
    └── config/
        └── page.tsx       →  /dashboard/config

Uma página é simplesmente um componente React exportado como default:

// app/sobre/page.tsx
export default function Sobre() {
  return (
    <main>
      <h1>Sobre nós</h1>
      <p>Somos uma empresa incrível.</p>
    </main>
  );
}

Salvou → http://localhost:3000/sobre já existe. Sem configurar nada.

17.2 Layouts: o que se repete entre páginas

Um layout.tsx envolve todas as páginas do seu nível para baixo — perfeito para cabeçalho, menu e rodapé. Ele recebe a página como children (lembra do Capítulo 10?):

// app/layout.tsx — layout RAIZ (obrigatório)
import "./globals.css";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <body>
        <header>
          <nav>{/* menu aqui */}</nav>
        </header>
        <main>{children}</main>   {/* ← a página atual entra aqui */}
        <footer>© 2026 Meu Site</footer>
      </body>
    </html>
  );
}

Layouts podem ser aninhados: um app/dashboard/layout.tsx adiciona uma barra lateral só às páginas do dashboard, por dentro do layout raiz. E ao navegar entre páginas, o layout não é re-renderizado — só o children troca.

17.3 Arquivos especiais do App Router

Arquivo Função
page.tsx O conteúdo da rota (torna a URL pública)
layout.tsx Moldura compartilhada (persiste entre navegações)
loading.tsx Tela de carregamento automática da rota
error.tsx Tela de erro automática da rota
not-found.tsx Página 404 personalizada
// app/blog/loading.tsx — aparece sozinho enquanto a página busca dados
export default function Loading() {
  return <p>Carregando posts... </p>;
}

17.4 Navegação com

⚠️ Nunca use <a href> para links internos — ele recarrega a página inteira e joga fora a mágica do Next.js. Use o componente Link:

import Link from "next/link";

export function Menu() {
  return (
    <nav>
      <Link href="/">Início</Link>
      <Link href="/sobre">Sobre</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
}

O Link faz navegação instantânea no cliente e ainda pré-carrega as páginas dos links visíveis na tela.

17.5 Rotas dinâmicas: [param]

Colchetes no nome da pasta criam um segmento dinâmico, que a página recebe via params:

// app/blog/[slug]/page.tsx
export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;   // no Next moderno, params é uma Promise
  return <h1>Post: {slug}</h1>;
}

Acessar /blog/aprendendo-nextjs mostra "Post: aprendendo-nextjs". No Capítulo 20 usaremos o slug para buscar o conteúdo real do post.

Variações: [...slug] captura múltiplos segmentos; (grupo) cria pastas organizacionais que não entram na URL.

17.6 Navegação programática: useRouter

Para navegar por código (após um login, por exemplo):

"use client";                          // explicação no Capítulo 18!
import { useRouter } from "next/navigation";

export function BotaoSair() {
  const router = useRouter();
  return <button onClick={() => router.push("/login")}>Sair</button>;
}

🧪 Exercícios — Capítulo 17

  1. Crie as rotas /, /sobre e /contato, com um menu no layout raiz usando Link.
  2. Crie a rota dinâmica /produtos/[id] que exibe "Produto nº {id}".
  3. Adicione um not-found.tsx estilizado na raiz e visite uma URL inexistente.
  4. Crie um app/dashboard/layout.tsx com uma barra lateral e duas páginas dentro (/dashboard e /dashboard/config). Navegue entre elas e observe que a barra não pisca (não re-renderiza).

Capítulo 18 — Server Components x Client Components

18.1 A grande novidade do Next.js moderno

No App Router, todo componente é, por padrão, um Server Component: ele executa no servidor, e só o HTML resultante viaja ao navegador. Componentes que precisam de interatividade viram Client Components com a diretiva "use client".

Server Component (padrão) Client Component ("use client")
Onde executa Servidor Servidor (1º render) + Navegador
Pode usar useState/useEffect
Pode usar onClick etc.
Pode acessar banco/segredos ❌ (nunca!)
Pode ser async + await fetch
Peso no JavaScript do cliente Zero Entra no bundle

18.2 Quando usar cada um

Regra prática: comece tudo como Server Component. Adicione "use client" apenas quando o arquivo precisar de interatividade (estado, eventos, efeitos, APIs do navegador).

// app/page.tsx — SERVER component (sem diretiva): busca dados direto
export default async function Home() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await res.json();

  return (
    <main>
      <h1>Blog</h1>
      {posts.slice(0, 5).map((p: { id: number; title: string }) => (
        <article key={p.id}>{p.title}</article>
      ))}
      <BotaoCurtir />   {/* ilha de interatividade */}
    </main>
  );
}
// components/BotaoCurtir.tsx — CLIENT component: tem estado e clique
"use client";
import { useState } from "react";

export function BotaoCurtir() {
  const [curtidas, setCurtidas] = useState(0);
  return <button onClick={() => setCurtidas(c => c + 1)}>❤️ {curtidas}</button>;
}

Repare no padrão profissional: a página é server (dados, SEO, zero JS) e os pedacinhos interativos são ilhas client. Quanto menor a ilha, melhor a performance.

18.3 Regras de composição

  1. "use client" vai na primeira linha do arquivo e "contamina" tudo que ele importa.
  2. Server pode renderizar Client (o normal). Client não pode importar Server — mas pode recebê-lo via children.
  3. Props de Server para Client devem ser serializáveis (dados simples; funções não passam — para isso existem Server Actions, Capítulo 21).

18.4 Erros que todo iniciante comete

🧪 Exercícios — Capítulo 18

  1. Crie uma página server que mostra a hora do servidor (new Date().toISOString()). Recarregue e pense: por que essa hora não atualiza sozinha como o relógio do Capítulo 13?
  2. Crie um <Acordeao> client (clica para expandir/recolher) e use-o numa página server, passando o conteúdo via children.
  3. Pegue o exercício da Loja (Cap. 12) e divida: página server com os dados + componente client só para busca e filtro.
  4. Force o erro do useState sem "use client" e leia a mensagem — reconhecê-la vai economizar horas da sua vida.

Capítulo 19 — Estilização no Next.js

19.1 As opções

Método Como funciona Quando usar
CSS global globals.css importado no layout raiz Reset, variáveis, estilos base
CSS Modules Arquivo.module.css → classes com escopo local Componentes, sem dependências
Tailwind CSS Classes utilitárias direto no JSX Padrão de mercado atual
CSS-in-JS / styled-components Estilo em JS Legado; evite em projetos novos com App Router

19.2 CSS Modules: seu CSS, com escopo automático

/* components/Cartao.module.css */
.cartao {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}
.titulo {
  color: #0c2840;
}
// components/Cartao.tsx
import styles from "./Cartao.module.css";

export function Cartao({ children }: { children: React.ReactNode }) {
  return (
    <div className={styles.cartao}>
      <h2 className={styles.titulo}>Cartão</h2>
      {children}
    </div>
  );
}

O Next.js gera nomes únicos (Cartao_cartao__x7Kd2), então classes de arquivos diferentes nunca conflitam. Todo o CSS que você já sabe funciona aqui.

19.3 Tailwind CSS: o padrão do mercado

Com o Tailwind, você estiliza com classes utilitárias, sem sair do JSX:

export function Cartao({ children }: { children: React.ReactNode }) {
  return (
    <div className="max-w-sm rounded-lg border border-gray-200 p-4 shadow-md hover:shadow-lg transition-shadow">
      {children}
    </div>
  );
}

Tradução das classes: largura máxima sm, cantos arredondados, borda cinza, padding 1rem, sombra média, sombra maior no hover, com transição. Seu conhecimento de CSS mapeia direto: p-4 = padding, flex = display flex, md:grid-cols-2 = grid de 2 colunas a partir do breakpoint md.

Prós: velocidade, consistência, responsividade fácil (sm: md: lg:), sem inventar nomes de classe. Contra: JSX visualmente carregado no começo. Vale o investimento — a maioria das vagas menciona Tailwind.

19.4 Imagens otimizadas: next/image

import Image from "next/image";

<Image
  src="/foto-perfil.jpg"      // arquivo em /public
  alt="Foto de perfil de Maria"
  width={200}
  height={200}
  priority                     // para imagens acima da dobra
/>

O componente Image redimensiona, converte para formatos modernos (WebP/AVIF), faz lazy loading e evita "pulos" de layout — otimizações que valem pontos preciosos de performance e SEO.

19.5 Fontes otimizadas: next/font

// app/layout.tsx
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({ subsets: ["latin"], weight: ["400", "700"] });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" className={montserrat.className}>
      <body>{children}</body>
    </html>
  );
}

A fonte é baixada no build e servida do seu próprio domínio: sem requisições ao Google em tempo de execução, sem flash de fonte errada.

🧪 Exercícios — Capítulo 19

  1. Crie um componente Botao com CSS Modules em três variantes (primário, secundário, perigo) escolhidas via prop.
  2. Recrie o mesmo botão com Tailwind. Compare os dois arquivos.
  3. Monte uma grade responsiva de cards com Tailwind: 1 coluna no celular, 2 no tablet (md:), 3 no desktop (lg:).
  4. Adicione uma imagem com next/image e uma fonte do Google com next/font ao seu projeto.

Capítulo 20 — Buscando dados: SSR, SSG, ISR e cache

20.1 Fetch direto no Server Component

A forma padrão de buscar dados no Next.js — sem useEffect, sem estados de carregamento manuais:

// app/posts/page.tsx
type Post = { id: number; title: string; body: string };

export default async function Posts() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  if (!res.ok) throw new Error("Falha ao buscar posts");   // cai no error.tsx
  const posts: Post[] = await res.json();

  return (
    <main>
      <h1>Posts</h1>
      {posts.map((p) => (
        <article key={p.id}>
          <h2>{p.title}</h2>
          <p>{p.body}</p>
        </article>
      ))}
    </main>
  );
}

Compare com o Capítulo 13: sem useState, sem useEffect, sem trio carregando/erro/dados. O componente async espera os dados no servidor e envia o HTML pronto. O loading.tsx (Cap. 17) cuida do "carregando" e o error.tsx do erro.

20.2 As três estratégias de renderização

A grande decisão de arquitetura: quando o HTML é gerado?

Estratégia Quando gera o HTML Ideal para
SSG (Static Site Generation) Uma vez, no build Blog, docs, landing pages, marketing
ISR (Incremental Static Regeneration) No build + regenera a cada X segundos E-commerce, notícias, catálogos
SSR (Server-Side Rendering) A cada requisição Dashboards, dados personalizados/tempo real

No App Router, você controla isso pelo cache do fetch:

// SSG — padrão para dados que não mudam (cache permanente)
const res = await fetch(url, { cache: "force-cache" });

// ISR — regenera no máximo a cada 60 segundos
const res = await fetch(url, { next: { revalidate: 60 } });

// SSR — sempre fresco, busca a cada requisição
const res = await fetch(url, { cache: "no-store" });

Também é possível definir para a rota inteira com uma exportação:

export const revalidate = 60;          // ISR na página toda
export const dynamic = "force-dynamic"; // SSR na página toda

💡 Regra de bolso: estático sempre que possível, dinâmico quando necessário. Página estática = servida de CDN = praticamente instantânea e baratíssima.

20.3 Páginas dinâmicas + generateStaticParams

Unindo rotas dinâmicas (Cap. 17) com SSG — gerar no build uma página por post:

// app/blog/[slug]/page.tsx
type Post = { id: number; title: string; body: string };

// Diz ao Next quais slugs existem para pré-gerar no build:
export async function generateStaticParams() {
  const posts: Post[] = await fetch("https://jsonplaceholder.typicode.com/posts")
    .then((r) => r.json());
  return posts.slice(0, 10).map((p) => ({ slug: String(p.id) }));
}

export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post: Post = await fetch(`https://jsonplaceholder.typicode.com/posts/${slug}`)
    .then((r) => r.json());

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  );
}

Rode npm run build e veja no resumo: 10 páginas de post geradas como estáticas ().

20.4 Streaming com Suspense

Se uma parte da página é lenta (uma consulta pesada), não segure a página inteira — transmita o resto e mostre um fallback na parte lenta:

import { Suspense } from "react";

export default function Pagina() {
  return (
    <main>
      <h1>Dashboard</h1>                {/* aparece imediatamente */}
      <Suspense fallback={<p>Carregando vendas...</p>}>
        <GraficoDeVendas />             {/* componente async lento */}
      </Suspense>
    </main>
  );
}

20.5 Metadata e SEO

Um dos grandes motivos de usar Next.js. Em qualquer page.tsx ou layout.tsx:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Blog | Meu Site",
  description: "Artigos sobre desenvolvimento web, React e Next.js.",
};

Para páginas dinâmicas, gere a metadata a partir dos dados:

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await buscarPost(slug);
  return {
    title: post.title,
    description: post.body.slice(0, 150),
    openGraph: { title: post.title },   // preview em redes sociais
  };
}

🧪 Exercícios — Capítulo 20

  1. Crie /usuarios listando os usuários da JSONPlaceholder (Server Component + loading.tsx).
  2. Crie /usuarios/[id] com os detalhes de cada usuário, linkada a partir da lista, com generateMetadata usando o nome do usuário como título.
  3. Faça uma página com a hora atual em três versões: force-cache, revalidate: 10 e no-store. Rode npm run build && npm start e observe o comportamento de cada uma ao recarregar.
  4. Explique para um "cliente fictício": por que o blog dele deveria ser SSG/ISR e não SSR?

Capítulo 21 — Backend no Next.js: Route Handlers e Server Actions

21.1 Route Handlers: sua API dentro do projeto

Um arquivo route.ts cria um endpoint HTTP — seu backend no mesmo repositório:

// app/api/mensagens/route.ts
import { NextResponse } from "next/server";

const mensagens: { id: number; texto: string }[] = [
  { id: 1, texto: "Olá!" },
];

// GET /api/mensagens
export async function GET() {
  return NextResponse.json(mensagens);
}

// POST /api/mensagens
export async function POST(request: Request) {
  const corpo = await request.json();
  if (!corpo.texto) {
    return NextResponse.json({ erro: "texto é obrigatório" }, { status: 400 });
  }
  const nova = { id: mensagens.length + 1, texto: corpo.texto };
  mensagens.push(nova);
  return NextResponse.json(nova, { status: 201 });
}

Acesse http://localhost:3000/api/mensagens no navegador (GET) ou consuma de um Client Component com fetch("/api/mensagens"). Usos típicos: webhooks, endpoints para apps mobile, integrações com serviços externos.

⚠️ O array acima vive na memória e some ao reiniciar — em produção usa-se banco de dados (Postgres, MongoDB...) com ferramentas como Prisma ou Drizzle. Fica como próximo passo pós-apostila.

21.2 Server Actions: funções do servidor chamadas do cliente

A forma moderna de mutações (criar/editar/apagar) — uma função com "use server" que o formulário chama diretamente, sem você criar endpoint nem escrever fetch:

// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function criarComentario(formData: FormData) {
  const texto = formData.get("texto") as string;
  if (!texto || texto.length < 3) return { erro: "Comentário muito curto" };

  await salvarNoBanco(texto);       // roda NO SERVIDOR: pode usar segredos, banco...
  revalidatePath("/comentarios");   // atualiza o cache da página
}
// app/comentarios/page.tsx (Server Component!)
import { criarComentario } from "../actions";

export default function Comentarios() {
  return (
    <form action={criarComentario}>
      <textarea name="texto" required />
      <button>Comentar</button>
    </form>
  );
}

Repare: um formulário funcional sem uma linha de JavaScript no cliente — funciona até com JS desabilitado. O Next.js cuida da chamada de rede por baixo dos panos.

21.3 Estado do formulário com useActionState

Para exibir erros e estado "enviando..." (aí sim, uma ilha client):

"use client";
import { useActionState } from "react";
import { criarComentario } from "../actions";

export function FormComentario() {
  const [estado, acao, pendente] = useActionState(criarComentario, null);

  return (
    <form action={acao}>
      <textarea name="texto" required />
      <button disabled={pendente}>{pendente ? "Enviando..." : "Comentar"}</button>
      {estado?.erro && <p className="erro">{estado.erro}</p>}
    </form>
  );
}

(Ao usar useActionState, a action passa a receber o estado anterior como primeiro parâmetro: criarComentario(estadoAnterior, formData).)

21.4 Variáveis de ambiente: guardando segredos

Chaves de API e senhas de banco nunca vão no código. Crie um arquivo .env.local (fora do Git!):

DATABASE_URL=postgres://usuario:senha@host/banco
API_SECRET=chave-super-secreta
NEXT_PUBLIC_SITE_URL=https://meusite.com
process.env.API_SECRET          // ✅ disponível só no servidor
process.env.NEXT_PUBLIC_SITE_URL // ⚠️ prefixo NEXT_PUBLIC_ = exposto ao navegador

⚠️ Regra de segurança: se não tem NEXT_PUBLIC_, só existe no servidor. Jamais coloque segredos em variáveis NEXT_PUBLIC_.

🧪 Exercícios — Capítulo 21

  1. Crie GET /api/horario que retorna { agora: new Date() } e teste no navegador.
  2. Crie um livro de visitas: Server Action que adiciona mensagens a um array em memória + página server que as lista + revalidatePath.
  3. Adicione validação (mínimo 3 caracteres) e exiba o erro com useActionState.
  4. Crie uma variável no .env.local e mostre seu valor numa página server. Depois tente acessá-la num client component e observe o que acontece.

Capítulo 22 — Deploy: colocando seu site no mundo

22.1 Preparação

npm run build

Se o build passa localmente (sem erros de TypeScript/ESLint), você está pronto. Garanta também que o projeto está no GitHub:

git init
git add .
git commit -m "primeiro commit"
# crie o repositório no github.com e siga as instruções de push

22.2 Deploy na Vercel (o caminho natural)

A Vercel é a criadora do Next.js e tem plano gratuito generoso (Hobby):

  1. Crie conta em vercel.com com seu GitHub.
  2. Add New → Project → importe o repositório.
  3. A Vercel detecta o Next.js sozinha → Deploy.
  4. Em ~1 minuto: https://seu-projeto.vercel.app no ar, com HTTPS e CDN global.

O fluxo contínuo é o melhor: todo git push na branch principal gera um novo deploy automático, e cada pull request ganha uma URL de preview.

Não esqueça: variáveis do .env.local devem ser cadastradas no painel (Settings → Environment Variables).

22.3 Alternativas

22.4 Checklist de produção

🧪 Exercícios — Capítulo 22

  1. Publique seu projeto de estudos na Vercel e compartilhe a URL com alguém.
  2. Faça uma alteração, git push, e cronometre até ela aparecer no ar.
  3. Rode seu site no PageSpeed Insights e anote as métricas (LCP, CLS). Pesquise o que significam.

PARTE 4 — PROJETO GUIADO: PORTFÓLIO + BLOG PROFISSIONAL

Hora de juntar tudo. Vamos construir um site pessoal com portfólio e blog — exatamente o tipo de projeto que serve de cartão de visita para as primeiras vagas. Siga na ordem; cada etapa usa conceitos dos capítulos anteriores (indicados entre parênteses).

Etapa 1 — Setup e estrutura (Cap. 16–17)

npx create-next-app@latest meu-portfolio
# TypeScript: Yes | Tailwind: Yes | App Router: Yes
cd meu-portfolio
npm run dev

Crie a estrutura de rotas:

app/
├── layout.tsx          ← layout raiz com <Header> e <Footer>
├── page.tsx            ← home
├── sobre/page.tsx
├── projetos/page.tsx
├── blog/
│   ├── page.tsx        ← lista de posts
│   └── [slug]/page.tsx ← post individual
├── contato/page.tsx
└── not-found.tsx

Etapa 2 — Layout e navegação (Cap. 17, 19)

// components/Header.tsx
import Link from "next/link";

const links = [
  { href: "/", rotulo: "Início" },
  { href: "/projetos", rotulo: "Projetos" },
  { href: "/blog", rotulo: "Blog" },
  { href: "/sobre", rotulo: "Sobre" },
  { href: "/contato", rotulo: "Contato" },
];

export function Header() {
  return (
    <header className="sticky top-0 border-b bg-white/80 backdrop-blur">
      <nav className="mx-auto flex max-w-4xl items-center justify-between p-4">
        <Link href="/" className="font-bold text-xl">Seu Nome</Link>
        <ul className="flex gap-6">
          {links.map((l) => (
            <li key={l.href}>
              <Link href={l.href} className="hover:text-blue-700">{l.rotulo}</Link>
            </li>
          ))}
        </ul>
      </nav>
    </header>
  );
}

Monte no app/layout.tsx com next/font (Cap. 19) e metadata global (Cap. 20):

import type { Metadata } from "next";
import { Montserrat } from "next/font/google";
import { Header } from "@/components/Header";
import "./globals.css";

const fonte = Montserrat({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: { default: "Seu Nome — Desenvolvedor Front-end", template: "%s | Seu Nome" },
  description: "Portfólio e blog sobre desenvolvimento web.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR" className={fonte.className}>
      <body>
        <Header />
        <main className="mx-auto max-w-4xl p-4">{children}</main>
        <footer className="border-t p-4 text-center text-sm text-gray-500">
          © 2026 Seu Nome
        </footer>
      </body>
    </html>
  );
}

✅ Checkpoint: navegação funcionando entre todas as páginas, sem recarregar.

Etapa 3 — Dados dos projetos (Cap. 5, 8, 12)

Sem banco de dados por enquanto — um módulo de dados tipado resolve:

// lib/projetos.ts
export type Projeto = {
  slug: string;
  titulo: string;
  descricao: string;
  tecnologias: string[];
  link?: string;
};

export const projetos: Projeto[] = [
  {
    slug: "loja-virtual",
    titulo: "Loja Virtual",
    descricao: "E-commerce fictício com carrinho e filtros.",
    tecnologias: ["Next.js", "TypeScript", "Tailwind"],
    link: "https://github.com/voce/loja",
  },
  // ...adicione os SEUS projetos dos exercícios!
];
// app/projetos/page.tsx (Server Component)
import { projetos } from "@/lib/projetos";

export const metadata = { title: "Projetos" };

export default function Projetos() {
  return (
    <section>
      <h1 className="text-3xl font-bold mb-6">Projetos</h1>
      <div className="grid gap-4 md:grid-cols-2">
        {projetos.map((p) => (
          <article key={p.slug} className="rounded-lg border p-4 hover:shadow-md transition-shadow">
            <h2 className="text-xl font-semibold">{p.titulo}</h2>
            <p className="text-gray-600">{p.descricao}</p>
            <ul className="mt-2 flex flex-wrap gap-2">
              {p.tecnologias.map((t) => (
                <li key={t} className="rounded bg-gray-100 px-2 py-1 text-xs">{t}</li>
              ))}
            </ul>
          </article>
        ))}
      </div>
    </section>
  );
}

Etapa 4 — Blog com arquivos Markdown (Cap. 7, 17, 20)

Posts como arquivos .md na pasta posts/ — simples e profissional para começar:

npm install gray-matter marked
<!-- posts/meu-primeiro-post.md -->
---
titulo: "Como saí do HTML/CSS para o Next.js"
data: "2026-07-01"
resumo: "Minha jornada de aprendizado."
---

Aqui vai o **conteúdo** do post em Markdown...
// lib/posts.ts
import fs from "fs";                    // só funciona no servidor!
import path from "path";
import matter from "gray-matter";

export type Post = {
  slug: string;
  titulo: string;
  data: string;
  resumo: string;
  conteudo: string;
};

const pasta = path.join(process.cwd(), "posts");

export function listarPosts(): Post[] {
  return fs.readdirSync(pasta)
    .filter((f) => f.endsWith(".md"))
    .map((arquivo) => {
      const slug = arquivo.replace(/\.md$/, "");
      const bruto = fs.readFileSync(path.join(pasta, arquivo), "utf-8");
      const { data, content } = matter(bruto);
      return { slug, conteudo: content, ...(data as Omit<Post, "slug" | "conteudo">) };
    })
    .sort((a, b) => b.data.localeCompare(a.data));
}

export function buscarPost(slug: string): Post | undefined {
  return listarPosts().find((p) => p.slug === slug);
}
// app/blog/[slug]/page.tsx
import { listarPosts, buscarPost } from "@/lib/posts";
import { marked } from "marked";
import { notFound } from "next/navigation";
import type { Metadata } from "next";

type Props = { params: Promise<{ slug: string }> };

export function generateStaticParams() {
  return listarPosts().map((p) => ({ slug: p.slug }));   // SSG! (Cap. 20)
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = buscarPost(slug);
  return { title: post?.titulo, description: post?.resumo };
}

export default async function PaginaPost({ params }: Props) {
  const { slug } = await params;
  const post = buscarPost(slug);
  if (!post) notFound();

  return (
    <article className="prose max-w-none">
      <h1>{post.titulo}</h1>
      <p className="text-gray-500">{new Date(post.data).toLocaleDateString("pt-BR")}</p>
      <div dangerouslySetInnerHTML={{ __html: marked(post.conteudo) }} />
    </article>
  );
}

Complete o app/blog/page.tsx listando os posts com Link para cada um (exercício!).

✅ Checkpoint: npm run build deve mostrar seus posts como páginas estáticas (●).

Etapa 5 — Formulário de contato com Server Action (Cap. 14, 21)

// app/contato/actions.ts
"use server";

export type EstadoForm = { ok?: boolean; erro?: string } | null;

export async function enviarContato(_estado: EstadoForm, formData: FormData): Promise<EstadoForm> {
  const nome = formData.get("nome") as string;
  const email = formData.get("email") as string;
  const mensagem = formData.get("mensagem") as string;

  if (!nome || !email.includes("@") || mensagem.length < 10) {
    return { erro: "Preencha todos os campos (mensagem com 10+ caracteres)." };
  }

  console.log("Novo contato:", { nome, email, mensagem });
  // Próximo nível: enviar e-mail de verdade (ex.: biblioteca Resend)
  return { ok: true };
}
// app/contato/FormContato.tsx
"use client";
import { useActionState } from "react";
import { enviarContato, type EstadoForm } from "./actions";

export function FormContato() {
  const [estado, acao, pendente] = useActionState<EstadoForm, FormData>(enviarContato, null);

  if (estado?.ok) return <p className="text-green-700">Mensagem enviada! </p>;

  return (
    <form action={acao} className="flex max-w-md flex-col gap-3">
      <input name="nome" placeholder="Nome" className="rounded border p-2" required />
      <input name="email" type="email" placeholder="E-mail" className="rounded border p-2" required />
      <textarea name="mensagem" placeholder="Mensagem" rows={5} className="rounded border p-2" required />
      <button disabled={pendente} className="rounded bg-blue-900 p-2 text-white disabled:opacity-50">
        {pendente ? "Enviando..." : "Enviar"}
      </button>
      {estado?.erro && <p className="text-red-600">{estado.erro}</p>}
    </form>
  );
}

Etapa 6 — Polimento e deploy (Cap. 19, 20, 22)

  1. Foto na página Sobre com next/image.
  2. loading.tsx no blog e not-found.tsx estilizado.
  3. Revisar metadata de todas as páginas.
  4. Testar responsividade no celular (DevTools → modo dispositivo).
  5. git push → deploy na Vercel → portfólio no ar! 🎉

Desafios de expansão (para o seu ritmo)


PARTE 5 — CARREIRA E MERCADO

O que o mercado espera de um dev Next.js júnior

Obrigatório (você acabou de aprender!):

Diferenciais que aparecem nas vagas (próximos estudos):

O portfólio vale mais que o currículo

Para quem não tem experiência formal, o GitHub é o currículo:

  1. 3 projetos bem feitos > 15 projetos de tutorial. Termine, publique, capriche no README (o que é, screenshot, link ao vivo, como rodar).
  2. Fuja do padrão: todo mundo tem to-do list. Resolva um problema seu: app para o negócio de um parente, ferramenta para seu hobby, site para uma ONG local.
  3. Deploy sempre: projeto sem link ao vivo praticamente não existe para o recrutador.
  4. Commits frequentes e mensagens claras mostram consistência — recrutadores olham o gráfico de atividade.

Estratégia de busca da primeira vaga

Como continuar evoluindo

  1. Construa em público: poste seus projetos e aprendizados.
  2. Leia código dos outros: repositórios open source de templates Next.js ensinam padrões profissionais.
  3. Documentação oficial primeiro: nextjs.org/docs e react.dev são as fontes da verdade; tutoriais envelhecem rápido.
  4. Aprofunde o fundamento, não só a ferramenta: JavaScript sólido sobrevive a qualquer troca de framework.
  5. Inglês técnico: a maior alavanca de salário da carreira. Comece lendo docs em inglês com tradutor do lado.

Expectativas realistas


GLOSSÁRIO

API (Application Programming Interface) — "Balcão de atendimento" entre sistemas: um pede dados/ações, o outro responde. Na web, geralmente via HTTP retornando JSON.

App Router — Sistema de rotas moderno do Next.js baseado na pasta app/, com layouts, Server Components e streaming. Substitui o antigo Pages Router.

Array — Lista ordenada de valores: [1, 2, 3].

Assíncrono — Código que não bloqueia a execução enquanto espera algo demorado (ex.: resposta de rede). Ver Promise, async/await.

async/await — Sintaxe para escrever código assíncrono de forma legível; await pausa a função até a Promise resolver.

Boolean — Tipo com dois valores: true ou false.

Build — Processo que transforma seu código-fonte na versão otimizada de produção (npm run build).

Bundle — O pacote de JavaScript enviado ao navegador. Quanto menor, mais rápido o site.

Callback — Função passada como argumento para outra função, para ser chamada depois.

CDN (Content Delivery Network) — Rede de servidores espalhados pelo mundo que entrega arquivos a partir do ponto mais próximo do usuário.

Client Component — Componente React com a diretiva "use client"; roda no navegador e pode usar estado, efeitos e eventos.

Componente — Função que retorna JSX; a unidade básica de construção de interfaces no React.

Const / Let — Formas de declarar variáveis: const não pode ser reatribuída; let pode.

Deploy — Publicar a aplicação em um servidor para o mundo acessar.

Destructuring — Sintaxe para extrair valores de objetos/arrays: const { nome } = usuario.

DOM (Document Object Model) — Representação viva do HTML na memória do navegador, manipulável via JavaScript.

ESLint — Ferramenta que analisa o código e aponta erros e más práticas automaticamente.

Estado (state) — Dados internos de um componente que mudam com o tempo e disparam re-renderização (useState).

Fetch — Função nativa para fazer requisições HTTP: await fetch(url).

Framework — Estrutura completa que organiza e executa seu código segundo suas convenções (Next.js). Compare com biblioteca, que você chama quando quer (React).

Hook — Função do React iniciada por use que adiciona capacidades a componentes (useState, useEffect, useContext...).

Hidratação (hydration) — Processo em que o React "liga" a interatividade sobre o HTML que veio pronto do servidor.

Imutabilidade — Prática de nunca modificar dados existentes, sempre criando cópias alteradas ([...array, novo]).

ISR (Incremental Static Regeneration) — Estratégia do Next.js: páginas estáticas que se regeneram sozinhas a cada X segundos (revalidate).

JSON — Formato de texto para troca de dados: {"nome": "Ana"}.

JSX — Sintaxe que mistura marcação HTML-like com JavaScript, usada para descrever interfaces no React.

Key — Prop especial e única exigida em listas (.map()) para o React identificar cada item.

Layout — Arquivo do Next.js (layout.tsx) com a "moldura" compartilhada entre páginas.

Middleware — Código que roda antes de a requisição chegar à página (redirecionamentos, autenticação...).

Node.js — Ambiente que executa JavaScript fora do navegador (servidores, ferramentas).

npm / npx — Gerenciador de pacotes do Node (npm install) e executor de pacotes (npx create-next-app).

Pages Router — Sistema de rotas antigo do Next.js (pasta pages/). Ainda existe em projetos legados.

Props — Dados que um componente recebe de seu pai, como atributos: <Card titulo="Oi" />. Somente leitura.

Promise — Objeto que representa um valor futuro (sucesso ou falha) de uma operação assíncrona.

Renderização — Processo de transformar componentes em elementos visíveis na tela.

Renderização condicional — Mostrar JSX diferente conforme uma condição (? :, &&, if).

Rota dinâmica — Rota com segmento variável: app/blog/[slug]/page.tsx atende /blog/qualquer-coisa.

Route Handler — Arquivo route.ts que cria um endpoint de API dentro do Next.js.

Server Action — Função com "use server" executada no servidor, chamável a partir de formulários/componentes.

Server Component — Componente que roda só no servidor (padrão no App Router); pode ser async, acessar banco e segredos; não usa hooks de estado.

SEO (Search Engine Optimization) — Práticas para o site ranquear bem em buscadores; grande motivação para SSR/SSG.

SPA (Single Page Application) — App onde o navegador carrega um HTML mínimo e o JavaScript constrói/troca as telas.

Spread (...) — Operador que espalha itens de arrays/objetos, muito usado para criar cópias: { ...obj, x: 1 }.

SSG (Static Site Generation) — HTML gerado no build, servido pronto. Máxima velocidade.

SSR (Server-Side Rendering) — HTML gerado a cada requisição no servidor. Dados sempre frescos.

String — Tipo de dado para textos: "olá".

Suspense — Componente React que exibe um fallback enquanto conteúdo assíncrono carrega (streaming).

Tailwind CSS — Framework de CSS utilitário: estilização via classes como flex, p-4, md:grid-cols-2.

Template literal — String com crases que aceita variáveis: `Olá, ${nome}`.

TypeScript (TS) — Superconjunto do JavaScript com tipos estáticos, checados antes de o código rodar.

useEffect — Hook para efeitos colaterais (timers, listeners, sincronizações) após a renderização.

useState — Hook que cria estado em um componente: const [valor, setValor] = useState(inicial).

Vercel — Empresa criadora do Next.js e plataforma de deploy otimizada para ele.


Palavra final

Você começou esta apostila sabendo estruturar e estilizar páginas. Se chegou até aqui fazendo os exercícios e o projeto, agora você sabe programá-las, conectá-las a dados e publicá-las para o mundo — com as mesmas ferramentas usadas por Netflix, TikTok, Nike e Notion.

O próximo passo não está em outra apostila: está no seu editor. Construa, quebre, conserte, publique. É assim que se vira profissional.

Bons códigos! 🚀