Partilhar: WhatsApp
aulify
UC UC00621 · T. Desenv. Software

Ficha 02 · React avançado

useEffect, Fetch, Router, Deploy
Versão · Aluno
Tempo · 45 minutos
Aluno(a)
Turma
Data

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:

  1. Build local para verificar que tudo funciona: bash npm run build npm run preview # testar localmente

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

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

  4. Deploy via dashboard (alternativa 2, recomendado):

  5. Login em vercel.com.
  6. Add New Project → Import Git Repository.
  7. Selecionar repo.
  8. Vercel auto-detecta Vite, framework preset = Vite.
  9. Click Deploy.
  10. Em segundos, URL público disponível.

  11. Deploys automáticos:

  12. Cada push para main → deploy de produção.
  13. 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.