React é uma biblioteca JavaScript criada pela Meta para construir interfaces de utilizador. A ideia central é dividir a UI em componentes reutilizáveis.
O React usa um Virtual DOM — uma representação em memória do DOM real. Quando o estado muda, o React calcula o mínimo de alterações necessárias e atualiza só o que mudou. Isto torna as aplicações muito eficientes.
// Sem React — manipulação manual do DOM
const btn = document.createElement("button");
btn.textContent = "Clica aqui";
btn.addEventListener("click", () => {
document.querySelector("#contador").textContent++;
});
// Com React — declarativo e mais simples
function Contador() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>
{count}
</button>;
}
JSX é uma extensão de sintaxe que permite escrever HTML dentro de JavaScript. O Babel transpila JSX para chamadas React.createElement().
// JSX — parece HTML mas é JavaScript
const elemento = <h1 className="titulo">Olá, mundo!</h1>;
// O que o Babel gera por baixo:
const elemento = React.createElement(
"h1",
{ className: "titulo" },
"Olá, mundo!"
);
// Regras importantes do JSX:
// 1. class → className
// 2. for → htmlFor (em labels)
// 3. Tem de ter um único elemento raiz
// 4. Expressões JS vão dentro de {}
// 5. Tags auto-fechadas: <img />, <br />
const nome = "Ana";
const ui = (
<div>
<h1>Olá, {nome}!</h1>
<p>{2 + 2} anos de experiência</p>
<img src="foto.jpg" alt="Foto" />
</div>
);
Componentes são funções JavaScript que retornam JSX. O nome começa sempre com letra maiúscula.
// Componente simples
function OlaMundo() {
return <h1>Olá, mundo!</h1>;
}
// Componente com lógica
function Relogio() {
const agora = new Date();
const hora = agora.toLocaleTimeString("pt-PT");
return (
<div className="relogio">
<p>Hora atual:</p>
<h2>{hora}</h2>
</div>
);
}
// Renderizar na página
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(
document.getElementById("root")
);
root.render(<Relogio />);
React torna a renderização condicional e de listas muito expressiva usando JSX e JavaScript puro.
// Renderização condicional
function Alerta({ tipo, mensagem }) {
// if/else antes do return
if (!mensagem) return null; // não renderiza nada
return (
<div className={`alerta alerta-${tipo}`}>
{/* Operador && — renderiza se verdade */}
{tipo === "erro" && <span>❌</span>}
{/* Ternário — um ou outro */}
{tipo === "sucesso"
? <span>✅ {mensagem}</span>
: <span>⚠️ {mensagem}</span>
}
</div>
);
}
// Listas — sempre com key única!
function ListaTarefas({ tarefas }) {
return (
<ul>
{tarefas.map(tarefa => (
<li key={tarefa.id}>
{tarefa.texto}
</li>
))}
</ul>
);
}
key se a lista pode ser reordenada ou filtrada. Usa sempre um ID único do próprio item.
CartaoPerfil que recebe nome, cargo e avatar como props e os apresenta num cartão.function CartaoPerfil({ nome, cargo, avatar }) {
// O teu JSX aqui
}
// Usar:
<CartaoPerfil nome="Ana Silva" cargo="Dev" avatar="🧑💻" />
function CartaoPerfil({ nome, cargo, avatar }) {
return (
<div className="cartao">
<span className="avatar">{avatar}</span>
<h2>{nome}</h2>
<p>{cargo}</p>
</div>
);
}
<ul>. Não te esqueças da prop key!function ListaItens({ itens }) {
// Usa .map() para renderizar <li>
}
<ListaItens itens={["Pão", "Leite", "Ovos"]} />
function ListaItens({ itens }) {
return (
<ul>
{itens.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
}
Props (abreviação de properties) são o mecanismo para passar dados de um componente pai para um componente filho. As props são só de leitura — um filho nunca deve modificar as props recebidas.
// Componente pai passa props
function App() {
return (
<div>
<Botao
texto="Guardar"
cor="azul"
desativado={false}
aoClicar={() => console.log("clique!")}
/>
</div>
);
}
// Componente filho recebe props
function Botao({ texto, cor, desativado, aoClicar }) {
return (
<button
className={`btn btn-${cor}`}
disabled={desativado}
onClick={aoClicar}
>
{texto}
</button>
);
}
Podes definir valores por defeito para props usando desestruturação ou defaultProps.
// Valores por defeito na desestruturação (recomendado)
function Avatar({
nome = "Utilizador",
src = "/avatar-default.png",
tamanho = 40
}) {
return (
<img
src={src}
alt={nome}
width={tamanho}
height={tamanho}
style={{ borderRadius: "50%" }}
/>
);
}
// Usar sem algumas props
<Avatar nome="Ana" /> // src e tamanho usam defeito
A prop especial children representa o conteúdo entre as tags de abertura e fecho de um componente. É a base da composição em React.
// Componente "contentor" com children
function Cartao({ titulo, children }) {
return (
<div className="cartao">
{titulo && <h3 className="cartao-titulo">{titulo}</h3>}
<div className="cartao-corpo">
{children}
</div>
</div>
);
}
// Usar com qualquer conteúdo
<Cartao titulo="O meu perfil">
<Avatar nome="Ana" />
<p>Desenvolvedora Full-Stack</p>
<Botao texto="Seguir" />
</Cartao>
Layout com um cabeçalho, área de conteúdo (children) e rodapé.function Layout({ titulo, children }) {
// Cabeçalho com titulo, secção principal com children, rodapé
}
<Layout titulo="A minha App">
<p>Conteúdo aqui</p>
</Layout>
function Layout({ titulo, children }) {
return (
<div>
<header><h1>{titulo}</h1></header>
<main>{children}</main>
<footer><p>© 2025</p></footer>
</div>
);
}
useState é o hook mais fundamental do React. Permite que componentes funcionais guardem e atualizem valores que causam re-renderização.
import { useState } from "react";
function Contador() {
// [valor atual, função para atualizar]
const [count, setCount] = useState(0);
// ^ valor inicial
return (
<div>
<p>Contagem: {count}</p>
<button onClick={() => setCount(count + 1)}>
+
</button>
<button onClick={() => setCount(count - 1)}>
-
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
);
}
Quando o novo estado depende do anterior, usa a forma funcional do setter. Para objetos e arrays, nunca mutes — cria sempre uma cópia.
// ✅ Forma funcional — usa o valor mais recente
setCount(prev => prev + 1);
// Imutabilidade com objetos
const [user, setUser] = useState({ nome: "Ana", idade: 28 });
// ❌ Errado — mutação direta
user.idade = 29;
setUser(user);
// ✅ Correto — criar nova cópia
setUser(prev => ({ ...prev, idade: 29 }));
// Imutabilidade com arrays
const [lista, setLista] = useState(["a", "b"]);
// Adicionar
setLista(prev => [...prev, "c"]);
// Remover por índice
setLista(prev => prev.filter((_, i) => i !== 1));
// Atualizar um item
setLista(prev => prev.map((item, i) =>
i === 0 ? "novo" : item
));
A forma mais comum de usar useState é para controlar o valor de campos de formulário — chamados componentes controlados.
function FormularioNome() {
const [nome, setNome] = useState("");
const [enviado, setEnviado] = useState(false);
function handleSubmit(e) {
e.preventDefault();
setEnviado(true);
}
if (enviado) {
return <p>Olá, {nome}! 👋</p>;
}
return (
<form onSubmit={handleSubmit}>
<label>
O teu nome:
<input
type="text"
value={nome}
onChange={e => setNome(e.target.value)}
/>
</label>
<button type="submit">Enviar</button>
</form>
);
}
Contador com botões para incrementar, decrementar e reset. O valor nunca pode ser negativo.import { useState } from "react";
function Contador() {
const [count, setCount] = useState(0);
// Adiciona botões e lógica
}
function Contador() {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<button onClick={() => setCount(c => Math.max(0, c - 1))}>-</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
useEffect serve para executar código após a renderização — chamadas API, subscrições, timers ou qualquer operação que afeta o mundo fora do componente.
import { useState, useEffect } from "react";
function TituloSincrono({ titulo }) {
// Efeito que sincroniza com o document.title
useEffect(() => {
document.title = titulo;
// Cleanup — executado antes do próximo efeito
return () => {
document.title = "App";
};
}, [titulo]); // dependências — re-executa quando titulo muda
return <h1>{titulo}</h1>;
}
// Casos de uso comuns:
// [] — só executa ao montar
useEffect(() => { buscarDados(); }, []);
// [id] — executa quando id muda
useEffect(() => { buscarUtilizador(id); }, [id]);
// sem array — executa em cada render (raro!)
useEffect(() => { console.log("render"); });
useContext permite partilhar dados entre componentes sem ter de passar props por cada nível da árvore — o chamado "prop drilling".
import { createContext, useContext, useState } from "react";
// 1. Criar o context
const TemaContext = createContext("claro");
// 2. Providenciar com o Provider
function App() {
const [tema, setTema] = useState("claro");
return (
<TemaContext.Provider value={{ tema, setTema }}>
<Pagina />
</TemaContext.Provider>
);
}
// 3. Consumir em qualquer componente filho
function BotaoTema() {
const { tema, setTema } = useContext(TemaContext);
return (
<button onClick={() => setTema(t => t === "claro" ? "escuro" : "claro")}>
Tema atual: {tema}
</button>
);
}
useReducer é uma alternativa ao useState para estado mais complexo. Segue o padrão Redux — uma função reducer define como o estado muda.
import { useReducer } from "react";
// Reducer — função pura que retorna novo estado
function reducer(state, action) {
switch (action.type) {
case "incrementar":
return { ...state, count: state.count + 1 };
case "decrementar":
return { ...state, count: Math.max(0, state.count - 1) };
case "reset":
return { count: 0 };
default:
return state;
}
}
function Contador() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: "incrementar" })}>+</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
useMemo memoriza o resultado de um cálculo pesado. useCallback memoriza uma função. Usa-os só quando há um problema real de desempenho.
import { useMemo, useCallback } from "react";
// useMemo — não recalcula se dependências não mudaram
const totalFiltrado = useMemo(() => {
console.log("a calcular..."); // só corre quando lista muda
return lista
.filter(item => item.ativo)
.reduce((acc, item) => acc + item.valor, 0);
}, [lista]);
// useCallback — mesma função entre renders
// Útil quando passas funções a componentes memorizados
const handleClick = useCallback(() => {
setSeleccionado(id);
}, [id]);
// Regra geral: NÃO optimizes prematuramente.
// Só usa useMemo/useCallback quando tens evidência
// de que algo é lento com o React DevTools Profiler.
import { useState, useEffect } from "react";
function DadosAPI() {
const [dados, setDados] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Vai buscar dados aqui
}, []);
if (loading) return <p>A carregar...</p>;
// Renderiza os dados
}
function DadosAPI() {
const [dados, setDados] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function buscar() {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const json = await res.json();
setDados(json);
} finally {
setLoading(false);
}
}
buscar();
}, []);
if (loading) return <p>A carregar...</p>;
return <div><h2>{dados?.title}</h2></div>;
}
Num componente controlado, o React é a única fonte de verdade para os valores dos inputs. Cada mudança atualiza o estado, e o estado controla o input.
function FormularioContato() {
const [form, setForm] = useState({
nome: "", email: "", mensagem: ""
});
const [erros, setErros] = useState({});
// Handler genérico para todos os inputs
function handleChange(e) {
const { name, value } = e.target;
setForm(prev => ({ ...prev, [name]: value }));
}
function validar() {
const errs = {};
if (!form.nome) errs.nome = "Nome é obrigatório";
if (!form.email.includes("@")) errs.email = "Email inválido";
return errs;
}
function handleSubmit(e) {
e.preventDefault();
const errs = validar();
if (Object.keys(errs).length > 0) {
setErros(errs); return;
}
console.log("Enviado:", form);
}
return (
<form onSubmit={handleSubmit}>
<input name="nome" value={form.nome} onChange={handleChange} />
{erros.nome && <span className="erro">{erros.nome}</span>}
<button type="submit">Enviar</button>
</form>
);
}
O padrão mais comum em React é três estados: loading, dados e erro. Gere os três para uma boa experiência de utilizador.
function ListaUtilizadores() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [erro, setErro] = useState(null);
useEffect(() => {
async function buscar() {
try {
const res = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
if (!res.ok) throw new Error(`Erro ${res.status}`);
const data = await res.json();
setUsers(data);
} catch (e) {
setErro(e.message);
} finally {
setLoading(false);
}
}
buscar();
}, []);
if (loading) return <p>⏳ A carregar...</p>;
if (erro) return <p style={{ color: "red" }}>❌ {erro}</p>;
return (
<ul>
{users.map(u => (
<li key={u.id}>{u.name} — {u.email}</li>
))}
</ul>
);
}
Quando repetes a mesma lógica em vários componentes, extrai-a para um hook personalizado. Começa sempre com "use".
// hooks/useFetch.js
import { useState, useEffect } from "react";
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelado = false; // evita race conditions
async function buscar() {
setLoading(true);
try {
const res = await fetch(url);
const json = await res.json();
if (!cancelado) setData(json);
} catch (e) {
if (!cancelado) setError(e.message);
} finally {
if (!cancelado) setLoading(false);
}
}
buscar();
return () => { cancelado = true; }; // cleanup
}, [url]);
return { data, loading, error };
}
// Usar em qualquer componente
function PostsList() {
const { data, loading, error } = useFetch(
"https://jsonplaceholder.typicode.com/posts"
);
if (loading) return <p>A carregar...</p>;
if (error) return <p>Erro: {error}</p>;
return <p>{data?.length} posts carregados</p>;
}
function FormRegisto() {
const [form, setForm] = useState({
nome: "", email: "", password: ""
});
// Implementa validação e submissão
}
function FormRegisto() {
const [form, setForm] = useState({ nome: "", email: "", password: "" });
const [erro, setErro] = useState("");
function handleChange(e) {
setForm(p => ({ ...p, [e.target.name]: e.target.value }));
}
function handleSubmit(e) {
e.preventDefault();
if (form.password.length < 6) {
setErro("Password deve ter pelo menos 6 caracteres");
return;
}
setErro("");
console.log("Registado:", form);
}
return (
<form onSubmit={handleSubmit}>
<input name="nome" placeholder="Nome" value={form.nome} onChange={handleChange} />
<input name="email" placeholder="Email" value={form.email} onChange={handleChange} />
<input name="password" type="password" placeholder="Password" value={form.password} onChange={handleChange} />
{erro && <p style={{ color: "red" }}>{erro}</p>}
<button type="submit">Registar</button>
</form>
);
}
Neste projeto final vais construir uma aplicação de gestão de tarefas completa, aplicando tudo o que aprendeste nos 11 módulos anteriores. A app incluirá adicionar, remover e filtrar tarefas, com persistência no localStorage.
App, FormularioTarefa, ListaTarefas, ItemTarefa, FiltrosTarefas.useReducer para gerir o estado das tarefas. Ações: ADICIONAR, REMOVER, TOGGLE_COMPLETA, EDITAR.useMemo para calcular as tarefas filtradas eficientemente.useEffect para guardar as tarefas no localStorage sempre que mudam, e para as recuperar ao iniciar a app.useFetch criado no módulo 11.// App.jsx — estrutura principal
import { useReducer, useMemo, useEffect } from "react";
const ACOES = {
ADICIONAR: "adicionar",
REMOVER: "remover",
TOGGLE: "toggle",
};
function reducer(tarefas, acao) {
switch (acao.type) {
case ACOES.ADICIONAR:
return [...tarefas, {
id: Date.now(), texto: acao.texto, completa: false
}];
case ACOES.REMOVER:
return tarefas.filter(t => t.id !== acao.id);
case ACOES.TOGGLE:
return tarefas.map(t =>
t.id === acao.id ? { ...t, completa: !t.completa } : t
);
default: return tarefas;
}
}
export default function App() {
const inicial = JSON.parse(
localStorage.getItem("tarefas") || "[]"
);
const [tarefas, dispatch] = useReducer(reducer, inicial);
const [filtro, setFiltro] = useState("todas");
const [busca, setBusca] = useState("");
// Persistência automática
useEffect(() => {
localStorage.setItem("tarefas", JSON.stringify(tarefas));
}, [tarefas]);
// Filtrar e pesquisar (memoizado)
const tarefasVisiveis = useMemo(() => {
return tarefas
.filter(t => {
if (filtro === "ativas") return !t.completa;
if (filtro === "completas") return t.completa;
return true;
})
.filter(t =>
t.texto.toLowerCase().includes(busca.toLowerCase())
);
}, [tarefas, filtro, busca]);
return (
<div className="app">
<h1>📋 As minhas tarefas</h1>
<FormularioTarefa
onAdicionar={texto => dispatch({ type: ACOES.ADICIONAR, texto })}
/>
<FiltrosTarefas
filtro={filtro}
onFiltro={setFiltro}
busca={busca}
onBusca={setBusca}
/>
<ListaTarefas
tarefas={tarefasVisiveis}
onToggle={id => dispatch({ type: ACOES.TOGGLE, id })}
onRemover={id => dispatch({ type: ACOES.REMOVER, id })}
/>
<p>{tarefas.filter(t => !t.completa).length} tarefas por fazer</p>
</div>
);
}
Completaste o curso de JavaScript + React!
Aprendeste desde variáveis básicas até componentes React com hooks avançados e um projeto completo. O próximo passo é construir algo real!