JavaScript, TypeScript, React e Next.js do zero — com exercícios, projeto guiado, glossário e dicas de carreira.
De páginas estáticas a aplicações web modernas: JavaScript, React e Next.js do zero.
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:
create-next-app ao deploy.Convenções usadas:
// JavaScript ou // TypeScript mostram a mesma ideia nas duas linguagens. Aprenda primeiro em JavaScript; o TypeScript é o mesmo código com anotações de tipo.Pré-requisitos técnicos:
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.
O Next.js usa JavaScript nos dois lugares ao mesmo tempo — por isso você precisa dominar a linguagem antes de chegar lá.
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. 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.
<body> para uma cor aleatória (pesquise Math.random).console.log(2 + 2). Depois digite document.title = "Mudei o título!" e veja o que acontece na aba do navegador.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.
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"
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.`;
// 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.
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.
"10" + 5, "10" - 5 e 10 === "10". Explique os resultados (pesquise "coerção de tipos" se ficar em dúvida).**. Mostre o resultado com console.log.const é preferível a let na maioria dos casos?const hora = 14;
if (hora < 12) {
console.log("Bom dia!");
} else if (hora < 18) {
console.log("Boa tarde!");
} else {
console.log("Boa noite!");
}
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".
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
}
Ú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.
// 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.
if/else como ternário: if (saldo > 0) { msg = "positivo" } else { msg = "negativo" }.["HTML", "CSS", "JavaScript"], use for...of para imprimir "Eu sei X" para cada item.if (0) { console.log("A") } else { console.log("B") }? Por quê?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!"
nome é um parâmetro (a variável de entrada)."Carlos" é o argumento (o valor real passado).return define o valor de saída e encerra a função.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;
const saudar = (nome = "visitante") => `Olá, ${nome}!`;
saudar(); // "Olá, visitante!"
saudar("Ana"); // "Olá, Ana!"
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.
// 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'
calcularDesconto(preco, percentual) que retorna o preço com desconto. Depois converta para arrow function.ehPar(n) que retorna true se o número for par (dica: operador %).repetir(vezes, fn) que chama a função fn o número de vezes indicado. Teste com repetir(3, () => console.log("oi")).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
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.
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);
},
};
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);
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"
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
// 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.
[10, 25, 30, 47, 52], use filter para obter só os maiores que 30 e depois map para dividi-los por 10.filmes (título, ano, nota). Use filter + map para obter os títulos dos filmes com nota ≥ 8.reduce para calcular a média das notas dos filmes.const config = { tema: "escuro", idioma: "pt" }, use destructuring para extrair as duas propriedades e spread para criar uma cópia com idioma: "en".type Filme em TypeScript para o exercício 2.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.
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.
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>
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.
input), quantos caracteres foram digitados.Enter no campo também adicionar a tarefa (evento keydown, tecla "Enter").dark no <body> (use classList.toggle e estilize a classe no CSS).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
export default: um por arquivo; importa-se sem chaves e com qualquer nome.export nomeado: vários por arquivo; importa-se com chaves e o nome exato.No React/Next.js, cada componente vive em seu próprio arquivo e usa exatamente essa sintaxe.
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.
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
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:
await só funciona dentro de funções async (ou no topo de módulos).await "pausa" a função até a Promise resolver — sem travar o resto da página.try/catch para tratar erros de rede.💡 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.
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
// 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>;
}
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).fetch + async/await para buscar https://jsonplaceholder.typicode.com/posts/1 e mostrar o título no console./posts (lista completa) e imprimir apenas os títulos dos 5 primeiros (dica: .slice(0, 5) + .map).await antes do fetch? Teste e explique.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.
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
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.
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.
// 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.
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
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.
type Livro com título, autor, ano e páginas (opcional). Crie um array Livro[] com 3 livros.type Prioridade = "baixa" | "media" | "alta" e uma função corDaPrioridade(p: Prioridade): string que retorna uma cor para cada valor (use switch).const aplicarDesconto = (preco, cupom) => cupom === "DEZ" ? preco * 0.9 : preco.p.bio.length, p.bio?.length e p.bio ?? "padrão".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).
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.
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:
{} inserem JavaScript no meio do "HTML": variáveis, expressões, ternários, .map().<>...</>.<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>
</>
);
}
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").
<div class="perfil"><img src="foto.png"><label for="nome">Nome</label></div>.Perfil que mostra seu nome em um <h1> e sua cidade em um <p>, usando variáveis com {}.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.
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).
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.
⚠️ 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.
// 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.
Avatar que recebe src e nome e renderiza <img> com alt correto.Botao que recebe children e uma prop variante ("primario" ou "secundario") e aplica classes CSS diferentes para cada uma.PerfilUsuario que recebe um objeto usuario (nome, cargo, foto) e o exibe usando o Avatar do exercício 1 dentro dele (composição!).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.
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:
setContagem(novoValor).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....
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.
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.
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.
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"
+, − e Zerar, que nunca fica negativo.disabled).{ id, texto, concluida }: clicar alterna concluida (risque com CSS) e um botão ✕ remove.<input> e um <h1> que mostra o texto digitado em tempo real. O estado deve morar no pai.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>
);
}
⚠️ 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.
key={produto.id}key={indice} — aceitável apenas para listas que nunca mudam de ordem/tamanho.key={Math.random()} — nunca! Muda a cada render e destrói a performance.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 />}.
// 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.
key correta.<select> para ordenar por preço (crescente/decrescente). Dica: [...filtrados].sort(...).Semaforo com estado "verde" | "amarelo" | "vermelho" e um botão "Avançar" que troca a cor exibida (ternário aninhado ou objeto de mapeamento).&& com uma lista vazia e depois corrija-a.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>;
}
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 |
// 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.
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);
}, []);
busca dentro do efeito mas não listá-la → dados desatualizados. O ESLint do React avisa; obedeça a ele.filtrados no Capítulo 12).document.title para refletir o valor de um contador (efeito com dependência).setInterval + limpeza correta.https://jsonplaceholder.typicode.com/users e mostre os nomes, com estados de carregamento e erro.<input> de busca ao exercício 3 que filtra os usuários sem novo fetch. A filtragem precisa de useEffect? Por quê?// 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.
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..." />;
}
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>;
}
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");
if, loops ou funções aninhadas.O ESLint (já configurado no Next.js) fiscaliza essas regras para você.
useRef para criar um botão "Ir para o formulário" que faz scroll até ele (ref.current.scrollIntoView()).useContadorRegressivo(segundos) que retorna o tempo restante e uma função reiniciar.Diante de um layout novo, o profissional segue este roteiro (baseado no guia oficial "Thinking in React"):
Para cada dado, pergunte:
useState.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
tarefas é estado. E o número de tarefas concluídas — deve ser estado? Justifique.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:
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 |
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).
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:
npm run dev — servidor de desenvolvimento.npm run build — gera a versão otimizada de produção.npm start — roda a versão de produção gerada pelo build.npm run lint — verifica problemas no código.💡 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.
meu-site e rode npm run dev.app/page.tsx: apague o conteúdo do return e coloque um <h1> com seu nome. Veja o Fast Refresh agir.npm run build e leia o resumo no terminal: quais rotas foram geradas? O que significa o símbolo ○ (Static)?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.
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.
| 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>;
}
⚠️ 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.
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.
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>;
}
/, /sobre e /contato, com um menu no layout raiz usando Link./produtos/[id] que exibe "Produto nº {id}".not-found.tsx estilizado na raiz e visite uma URL inexistente.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).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 |
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.
"use client" vai na primeira linha do arquivo e "contamina" tudo que ele importa.children.useState sem "use client" → erro claro no terminal: "You're importing a component that needs useState...". Solução: extraia a parte interativa para um arquivo client pequeno."use client" no layout raiz "para resolver tudo" → transforma o site inteiro em client e joga fora as vantagens do Next.js.onClick num Server Component → mesmo caso: extraia uma ilha.new Date().toISOString()). Recarregue e pense: por que essa hora não atualiza sozinha como o relógio do Capítulo 13?<Acordeao> client (clica para expandir/recolher) e use-o numa página server, passando o conteúdo via children.useState sem "use client" e leia a mensagem — reconhecê-la vai economizar horas da sua vida.| 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 |
/* 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.
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.
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.
// 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.
Botao com CSS Modules em três variantes (primário, secundário, perigo) escolhidas via prop.md:), 3 no desktop (lg:).next/image e uma fonte do Google com next/font ao seu projeto.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.
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.
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 (●).
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>
);
}
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
};
}
/usuarios listando os usuários da JSONPlaceholder (Server Component + loading.tsx)./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.force-cache, revalidate: 10 e no-store. Rode npm run build && npm start e observe o comportamento de cada uma ao recarregar.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.
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.
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).)
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_.
GET /api/horario que retorna { agora: new Date() } e teste no navegador.revalidatePath.useActionState..env.local e mostre seu valor numa página server. Depois tente acessá-la num client component e observe o que acontece.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
A Vercel é a criadora do Next.js e tem plano gratuito generoso (Hobby):
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).
npm run build && npm start com Node/Docker; mais controle, mais responsabilidade.output: "export") — vira HTML puro para qualquer hospedagem, mas perde SSR, Server Actions e afins.npm run build sem erros e sem warnings importantesnext/image e alt preenchidonot-found.tsx)git push, e cronometre até ela aparecer no ar.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).
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
// 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.
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>
);
}
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 (●).
// 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>
);
}
next/image.loading.tsx no blog e not-found.tsx estilizado.git push → deploy na Vercel → portfólio no ar! 🎉dark:).Obrigatório (você acabou de aprender!):
Diferenciais que aparecem nas vagas (próximos estudos):
Para quem não tem experiência formal, o GitHub é o currículo:
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.
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! 🚀