Ficha 02 · React avançado
Exercício 1 · Relógio com useEffect
Cria um componente <Relogio /> que mostra a hora actual e actualiza a cada segundo. Usa useEffect com cleanup.
Resposta:
import { useState, useEffect } from "react";
function Relogio() {
const [hora, setHora] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setHora(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
return <h1>{hora.toLocaleTimeString("pt-PT")}</h1>;
}
Exercício 2 · Fetch de utilizadores GitHub
Cria um componente <Users /> que faz fetch a https://api.github.com/users?per_page=10 no mount e mostra uma lista com avatar e login de cada user. Inclui estado de loading e error.
Resposta:
import { useState, useEffect } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function load() {
try {
const r = await fetch("https://api.github.com/users?per_page=10");
if (!r.ok) throw new Error("Erro " + r.status);
const data = await r.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) return <p>A carregar...</p>;
if (error) return <p>Erro: {error}</p>;
return (
<ul style={{ listStyle: "none", padding: 0 }}>
{users.map(u => (
<li key={u.id} style={{ display: "flex", gap: 10, marginBottom: 10 }}>
<img src={u.avatar_url} alt={u.login} width="40" style={{ borderRadius: "50%" }} />
<span>{u.login}</span>
</li>
))}
</ul>
);
}
Exercício 3 · Busca dinâmica
Adiciona ao exercício anterior um input de pesquisa. À medida que o user escreve, filtra a lista de utilizadores mostrados (case insensitive).
Resposta:
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [filtro, setFiltro] = useState("");
useEffect(() => {
fetch("https://api.github.com/users?per_page=30")
.then(r => r.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
const filtrados = users.filter(u =>
u.login.toLowerCase().includes(filtro.toLowerCase())
);
if (loading) return <p>A carregar...</p>;
return (
<div>
<input
type="search"
value={filtro}
onChange={(e) => setFiltro(e.target.value)}
placeholder="Pesquisar..."
/>
<p>{filtrados.length} resultado(s)</p>
<ul>
{filtrados.map(u => (
<li key={u.id}>
<img src={u.avatar_url} alt={u.login} width="30" />
{u.login}
</li>
))}
</ul>
</div>
);
}
Exercício 4 · Persistência com localStorage
Modifica a TodoApp do exercício 6 da Ficha 01 para que as tarefas sejam guardadas no localStorage e carregadas no arranque.
Resposta:
import { useState, useEffect } from "react";
function TodoApp() {
// Carregar do localStorage no mount
const [todos, setTodos] = useState(() => {
const saved = localStorage.getItem("todos");
return saved ? JSON.parse(saved) : [];
});
const [input, setInput] = useState("");
// Guardar sempre que todos muda
useEffect(() => {
localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
const adicionar = (e) => {
e.preventDefault();
if (!input.trim()) return;
setTodos([...todos, { id: Date.now(), texto: input, feito: false }]);
setInput("");
};
const toggle = (id) => {
setTodos(todos.map(t => t.id === id ? { ...t, feito: !t.feito } : t));
};
const remover = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
return (
<div>
<form onSubmit={adicionar}>
<input value={input} onChange={(e) => setInput(e.target.value)} />
<button>Add</button>
</form>
<ul>
{todos.map(t => (
<li key={t.id}>
<input type="checkbox" checked={t.feito} onChange={() => toggle(t.id)} />
<span style={{ textDecoration: t.feito ? "line-through" : "none" }}>
{t.texto}
</span>
<button onClick={() => remover(t.id)}>X</button>
</li>
))}
</ul>
</div>
);
}
Usa useState(() => ...) para inicialização lazy (só corre uma vez).
Exercício 5 · React Router básico
Instala o React Router e cria 3 páginas: Home (/), Sobre (/sobre) e Contacto (/contacto). Adiciona uma <nav> com links para todas elas. Mostra-as conforme o URL.
Resposta:
npm install react-router-dom
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function Home() {
return <h1>Página inicial</h1>;
}
function Sobre() {
return <h1>Sobre nós</h1>;
}
function Contacto() {
return <h1>Contacto</h1>;
}
function App() {
return (
<BrowserRouter>
<nav style={{ display: "flex", gap: 20, padding: 20, background: "#eee" }}>
<Link to="/">Home</Link>
<Link to="/sobre">Sobre</Link>
<Link to="/contacto">Contacto</Link>
</nav>
<main style={{ padding: 20 }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/sobre" element={<Sobre />} />
<Route path="/contacto" element={<Contacto />} />
<Route path="*" element={<h1>404 — Não encontrado</h1>} />
</Routes>
</main>
</BrowserRouter>
);
}
Exercício 6 · Rotas dinâmicas
Cria uma rota /user/:id que faz fetch a https://api.github.com/users/{login} (usa o id como login) e mostra avatar, nome, bio e número de seguidores. Adiciona links da lista de users (exercício 2) para esta rota.
Resposta:
import { useEffect, useState } from "react";
import { useParams, Link } from "react-router-dom";
function UserDetail() {
const { id } = useParams();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`https://api.github.com/users/${id}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [id]);
if (loading) return <p>A carregar...</p>;
if (!user) return <p>User não encontrado.</p>;
return (
<div>
<Link to="/users">← Voltar</Link>
<img src={user.avatar_url} alt={user.login} width="100" />
<h1>{user.name || user.login}</h1>
<p>{user.bio}</p>
<p>Seguidores: {user.followers}</p>
</div>
);
}
// No componente Users — fazer o login um Link:
<li key={u.id}>
<Link to={`/user/${u.login}`}>{u.login}</Link>
</li>
// Em App, adicionar a rota:
<Route path="/user/:id" element={<UserDetail />} />
Importante: o array de dependências [id] faz o efeito correr sempre que o parâmetro muda.
Exercício 7 · Dark mode com Context
Implementa um sistema de dark/light mode usando Context API que possa ser consumido por qualquer componente sem prop drilling.
Resposta:
import { createContext, useContext, useState, useEffect } from "react";
// Criar contexto
const ThemeContext = createContext();
// Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() =>
localStorage.getItem("theme") || "light"
);
useEffect(() => {
localStorage.setItem("theme", theme);
document.body.style.background = theme === "dark" ? "#222" : "#fff";
document.body.style.color = theme === "dark" ? "#fff" : "#000";
}, [theme]);
const toggle = () => setTheme(t => t === "dark" ? "light" : "dark");
return (
<ThemeContext.Provider value={{ theme, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// Hook helper
function useTheme() {
return useContext(ThemeContext);
}
// Componente que consome
function ThemeToggle() {
const { theme, toggle } = useTheme();
return (
<button onClick={toggle}>
Mudar para {theme === "dark" ? "light" : "dark"}
</button>
);
}
// App
function App() {
return (
<ThemeProvider>
<div>
<h1>Olá!</h1>
<ThemeToggle />
</div>
</ThemeProvider>
);
}
Exercício 8 · Deploy no Vercel
Faz o build do projecto e deploy público no Vercel. Em meia página descreve o processo (comandos usados, configuração) e partilha o URL final.
Resposta:
Passos:
-
Build local para verificar que tudo funciona:
bash npm run build npm run preview # testar localmente -
Push para GitHub:
bash git init git add . git commit -m "Initial commit" git remote add origin https://github.com/user/repo.git git push -u origin main -
Deploy via CLI (alternativa 1):
bash npm install -g vercel vercel # Seguir prompts: link to existing? N | project name | etc. vercel --prod # promover para production -
Deploy via dashboard (alternativa 2, recomendado):
- Login em vercel.com.
- Add New Project → Import Git Repository.
- Selecionar repo.
- Vercel auto-detecta Vite, framework preset = Vite.
- Click Deploy.
-
Em segundos, URL público disponível.
-
Deploys automáticos:
- Cada push para
main→ deploy de produção. - Cada PR → preview URL único.
Configuração:
- Build command: npm run build (default).
- Output directory: dist (Vite default).
- Node version: 18+ (default).
URL típico: https://meu-projecto.vercel.app ou domínio custom.
Vantagens Vercel: - HTTPS automático. - CDN global. - Preview por PR. - Analytics built-in. - Free tier generoso para projectos pessoais.