Vamos transformar o componente Home em uma tela que consulta usuários públicos do GitHub. O trabalho tem duas partes independentes:

  1. Quando a página aparece, carregar uma lista de usuários.
  2. Quando alguém informar um login e pesquisar, consultar aquele usuário específico.

Não vamos criar um projeto novo nem alterar as rotas que já funcionam. O arquivo principal desta prática será src/routes/Home/index.tsx.

O resultado esperado

A tela apresentará um campo de pesquisa, um botão, mensagens de carregamento, mensagens de erro e uma lista com identificadores, logins e avatares. A pesquisa exibirá o login encontrado sem substituir a lista inicial.

Antes de começar

Você precisa ter:

Abra o terminal na pasta que contém my-app:

cd my-app
npm run dev

cd muda a pasta atual. npm run dev executa o script dev configurado no package.json. Abra o endereço que o terminal apresentar; não presuma que a porta será sempre a mesma.

Como utilizar este material

Os pequenos blocos das etapas intermediárias explicam partes do código. Não cole todas as versões em sequência dentro do mesmo arquivo. Na etapa 18 existe a versão completa, destinada a substituir o conteúdo de src/routes/Home/index.tsx.

Cada exercício deve ser tentado antes da leitura do gabarito. Errar durante a prática ajuda a localizar o conceito que ainda precisa de atenção.

Mantenha os arquivos de rota existentes:

Arquivo

Responsabilidade nesta aplicação

src/main.tsx

Configura createBrowserRouter e renderiza RouterProvider.

src/App.tsx

Mantém Cabecalho, Outlet e Rodape.

src/routes/Home/index.tsx

Recebe o código desta prática.

src/routes/Produtos/index.tsx

Permanece como está.

src/routes/EditarProdutos/index.tsx

Permanece como está.

src/routes/Error/index.tsx

Continua configurado em errorElement.

As rotas continuam sendo /, /produtos e /editar-produtos/:id.

O Outlet de App apresenta a rota filha. Quando a URL é /, ele apresenta Home. Não adicione outro BrowserRouter, outro RouterProvider ou novas rotas para realizar esta prática.

Duas URLs com funções diferentes

Exemplo

Quem atende?

Para que serve?

/

O roteador da aplicação

Apresenta Home.

https://api.github.com/users

O servidor do GitHub

Fornece dados de usuários.

A URL da API não deve entrar no array de rotas do React. Ela será usada pelo fetch.

O componente Error recebe todos os erros?

Não. Um erro capturado no catch da pesquisa será tratado pelo próprio componente. Não é necessário navegar para outra página para dizer que um login não foi encontrado.

Pratique: explique por que a rota / e a URL da API não são a mesma coisa.

Uma API é uma interface de comunicação entre programas. Nesta prática, o navegador solicita informações e o servidor do GitHub devolve uma resposta.

Um endpoint é um endereço de uma operação da API. Usaremos dois:

Endpoint

Resultado esperado

https://api.github.com/users

Uma lista de usuários, representada por um array.

https://api.github.com/users/octocat

Um objeto representando o usuário solicitado.

O segundo endereço consulta um login exato. Não é uma busca por parte do nome, nome completo, e-mail ou biografia. Já a listagem é paginada: não representa todos os usuários do GitHub. Consulte os contratos dos endpoints na documentação do GitHub.

O que é HTTP?

É o protocolo usado nesta troca. A resposta inclui um código de status, cabeçalhos e um corpo. Um status de sucesso não é o próprio conteúdo; ele apenas informa o resultado da operação HTTP.

O que é JSON?

É um formato textual de dados. Exemplo ilustrativo, não uma cópia completa da resposta real:

{
  "login": "usuario-exemplo",
  "id": 123,
  "avatar_url": "https://example.com/avatar.png"
}

No JSON, nomes das propriedades ficam entre aspas duplas. O número 123 não está entre aspas porque é numérico.

Quando existem vários objetos, usamos uma lista:

[
  { "login": "ana", "id": 1, "avatar_url": "https://example.com/ana.png" },
  { "login": "bia", "id": 2, "avatar_url": "https://example.com/bia.png" }
]

Essas URLs de exemplo não são imagens para baixar. Os avatares da prática virão do campo avatar_url da resposta real.

Comparando com o arquivo TypeScript local

Arquivo .ts importado

API pela rede

Os dados fazem parte do código da aplicação.

Os dados vêm de outro sistema.

Um import fornece o valor exportado.

Uma requisição precisa terminar.

Não existe status HTTP nesse acesso local.

A resposta possui status HTTP.

Não há espera de rede para acessar o array importado.

Precisamos lidar com espera e falhas.

Neste codelab, async e await são parte central do conteúdo porque agora existe comunicação de rede.

O código original utiliza este tipo:

type TipoUserGit = {
  login: string;
  id: number;
  avatar_url: string;
};

Leia como: "Um valor do tipo TipoUserGit precisa ter estas três propriedades, com estes tipos".

Trecho

Explicação

type

Declara um nome para um tipo.

TipoUserGit

Nome escolhido para esse contrato.

{ ... }

Descreve as propriedades do objeto.

login: string

O login é texto.

id: number

O identificador é numérico.

avatar_url: string

O endereço da imagem é texto, não o arquivo da imagem.

Os nomes correspondem aos campos recebidos. Trocar avatar_url por avatarUrl apenas no tipo não transforma a resposta do servidor.

O tipo cria um usuário?

Não. Para criar um valor de exemplo seria necessário escrever um objeto:

const exemplo: TipoUserGit = {
  login: "ana",
  id: 1,
  avatar_url: "https://example.com/ana.png",
};

O tipo descreve. O objeto contém os dados.

Dentro ou fora do componente?

Declarar o tipo dentro de Home, como no código inicial, é válido. Na versão completa, ele ficará acima do componente para separar o contrato da lógica da tela. Isso é organização, não uma exigência do React.

E os outros campos da API?

Um objeto recebido pode ter outras propriedades. Esse tipo descreve apenas o subconjunto utilizado. A anotação não apaga campos extras e não verifica, sozinha, os dados que chegam pela rede.

Pratique: por que id: "123" não atende ao contrato? Como corrigir o valor sem mudar o tipo?

Observe:

const [usuarios, setUsuarios] = useState<TipoUserGit[]>([]);

Existem três ideias diferentes nessa linha:

Trecho

Papel

[usuarios, setUsuarios]

Desestrutura o par devolvido pelo hook.

Informa que o estado armazenará um array de usuários.

([])

Define o valor inicial: um array vazio.

Não confunda o [] do tipo com o [] do valor inicial. Um descreve o formato; o outro é o valor usado antes da resposta.

O par devolvido pelo hook

usuarios permite ler o estado da renderização atual. setUsuarios solicita sua atualização. Depois de setUsuarios(data), uma nova renderização pode apresentar a lista recebida. A variável da renderização em andamento não muda imediatamente. Essa é a base do funcionamento descrito na referência de useState.

setUsuarios(data);
// Aqui, usuarios ainda pertence à renderização atual.
// Para inspecionar a resposta recém-recebida, use console.log(data).

Por que não usar uma variável comum?

Alterar uma variável comum não solicita ao React que atualize a interface. Já o estado participa do processo de renderização.

Dois estados que não são a mesma coisa

const [user, setUser] = useState<string>("");
const [digitado, setDigitado] = useState<string>("");

Estado

O que guarda

Quem o altera

usuarios

Lista inicial

Resposta da listagem.

digitado

Conteúdo atual do campo

Evento onChange.

user

Login encontrado

Resposta da pesquisa.

Manteremos user como texto, assim como no exemplo inicial. Depois haverá um exercício para guardar o objeto completo.

useState("") já permite inferir string. Escrever aqui é opcional; foi mantido para facilitar a leitura dos tipos.

Pratique: antes de qualquer resposta, quais são os valores dos três estados?

Não colocaremos tudo no mesmo efeito.

Momento

Lista inicial

Pesquisa individual

Entrada em Home

O efeito inicia a consulta.

Nenhuma consulta individual.

Digitação

A lista permanece.

Apenas digitado muda.

Envio do formulário

A lista permanece.

buscador consulta o login informado.

Resposta bem-sucedida

Atualiza usuarios.

Atualiza user.

Falha

Atualiza erroLista.

Atualiza erroBusca.

Renderizar não é pesquisar

Renderizar significa executar o componente para calcular o JSX. Digitar pode provocar uma nova renderização. Isso não deve significar "pesquisar na API a cada tecla" nesta atividade.

Não escreva uma chamada de rede diretamente no corpo de Home:

// Não use como implementação:
function Home() {
  fetch("https://api.github.com/users");
  return <main>Home</main>;
}

Isso iniciaria novas requisições durante renderizações. O fluxo inicial pertence ao efeito; a pesquisa solicitada pela pessoa pertence ao manipulador do formulário.

Esta é a estrutura do efeito original:

useEffect(() => {
  async function loadingData() {
    // Aqui ficará o carregamento.
  }

  loadingData();
}, []);

Parte

Leitura

useEffect

Registra a sincronização com um sistema externo.

() => { ... }

Função de configuração do efeito.

loadingData

Função interna que realiza o trabalho assíncrono.

loadingData()

Executa a função; declarar não basta.

[]

Não há dependências reativas variáveis nesta configuração.

O efeito é executado depois que o componente é confirmado na interface. Na entrada em Home, iniciaremos o carregamento. Uma remontagem pode executá-lo novamente; em desenvolvimento, StrictMode também pode realizar um ciclo adicional de configuração e limpeza. Portanto, evite interpretar [] como "uma vez para sempre". Veja a referência de useEffect.

Dependências não são um botão de velocidade

Forma

Comportamento

Sem segundo argumento

Executa após cada confirmação de renderização.

[]

Não repete por mudanças de dependências.

[digitado]

Repete quando digitado muda, além da execução inicial.

Neste exemplo, o endereço da lista é fixo e o efeito não lê digitado. Não precisamos incluir o campo da pesquisa nas dependências.

Não omita uma dependência apenas para silenciar o comportamento. Se o efeito passar a ler um estado, sua estrutura deverá ser reavaliada.

Pratique: o que aconteceria se a busca individual fosse feita em um efeito com [digitado]?

Uma operação de rede não termina no mesmo instante em que é solicitada. Precisamos lidar com um resultado futuro.

async function loadingData() {
  const response = await fetch("https://api.github.com/users");
  const data = await response.json();
  console.log(data);
}

Esse trecho demonstra a sequência; o tratamento de falhas será acrescentado a seguir.

O que async informa?

Uma função async sempre devolve uma Promise: um objeto que representa a conclusão futura da operação. Mesmo sem escrever new Promise, estamos utilizando esse mecanismo. Neste codelab não criaremos Promises manualmente nem encadearemos .then(); utilizaremos await.

O que await faz?

Suspende a continuação daquela função até que o resultado esteja disponível. Não congela toda a página. Enquanto a rede trabalha, o navegador pode continuar atendendo outras tarefas.

Quando a operação termina com rejeição, await permite que a falha seja capturada pelo try/catch que envolve a chamada. Veja a explicação de funções async.

Por que não escrever useEffect(async () => ...)?

A função de configuração do efeito pode retornar uma função de limpeza ou não retornar nada. Uma função async retorna uma Promise. Por isso, mantemos a configuração síncrona e declaramos a função assíncrona dentro dela:

useEffect(() => {
  async function loadingData() {
    // O await fica aqui dentro.
  }

  loadingData();
}, []);

Declarar e chamar são ações diferentes

async function loadingData() { ... } cria a função. loadingData() inicia sua execução. Se a chamada for esquecida, não haverá carregamento.

Pratique: em qual função o await suspende a execução: Home, a página inteira ou loadingData?

const response = await fetch("https://api.github.com/users");
const data: TipoUserGit[] = await response.json();

Primeira espera: obter a resposta

fetch inicia uma requisição HTTP. Sem configuração de método, usa GET, adequado para consultar os dados desta prática.

O resultado aguardado é um objeto Response. Ele não é a lista de usuários. Nele podemos inspecionar status, ok e cabeçalhos, além de acessar o corpo. A documentação de Fetch detalha essa distinção.

Segunda espera: ler e interpretar o corpo

response.json() lê o corpo e interpreta seu conteúdo como JSON. O resultado pode ser um array ou um objeto, conforme o endpoint. A leitura também é assíncrona. Consulte Response.json.

Variável

Contém

Uso nesta prática

response

Resposta HTTP

Conferir sucesso antes de continuar.

data

Valor interpretado do corpo

Atualizar o estado.

Não escreva setUsuarios(response): o estado espera um array, não o envelope da resposta HTTP.

Erro proposital

// Não use:
const data = response.json();
setUsuarios(data);

Sem o segundo await, data representa uma Promise, não o array pronto.

O corpo pode ser lido várias vezes?

Não reutilize a mesma resposta chamando response.json() repetidamente. Guarde o valor em data e reutilize esse valor para log, validação e atualização do estado.

Pratique: explique a diferença entre response.ok e data.login.

Esta sequência precisa permanecer dentro do try:

try {
  const response = await fetch("https://api.github.com/users");

  if (!response.ok) {
    throw new Error("O carregamento da lista de usuários falhou!");
  }

  const data: TipoUserGit[] = await response.json();
  setUsuarios(data);
} catch (error) {
  console.error(error);
}

Por que verificar ok?

Uma resposta HTTP como 404 pode chegar normalmente ao navegador. fetch não rejeita automaticamente só porque o status indica falha HTTP. response.ok informa se o status está entre 200 e 299. O operador ! inverte o valor: !response.ok significa "a resposta não indica sucesso".

O papel de throw

new Error(...) cria um objeto de erro. throw interrompe o caminho normal do bloco e lança esse erro. O catch assume o tratamento; as linhas restantes do try são puladas.

Três falhas diferentes

Falha

Onde aparece

Exemplo de tratamento

Comunicação de rede

Durante await fetch(...)

Avisar que a consulta falhou.

HTTP sem sucesso

Na verificação de response.ok

Lançar uma mensagem apropriada ao status.

Corpo inválido para JSON

Durante await response.json()

Capturar a falha de interpretação.

O problema na busca original

No código inicial, o fetch da busca estava antes do try:

// Estrutura problemática:
const response = await fetch(url);
try {
  // ...
} catch (error) {
  console.error(error);
}

Se a comunicação falhar na primeira linha, esse catch não captura a rejeição, pois o programa ainda não entrou no bloco protegido. A correção é mover a chamada para dentro dele.

Console não substitui uma mensagem na tela

console.error ajuda durante o desenvolvimento. A pessoa que está usando a aplicação normalmente não abre o console. Por isso, também guardaremos uma mensagem em estado.

Adicione estados separados para os dois fluxos:

const [carregandoLista, setCarregandoLista] = useState(true);
const [erroLista, setErroLista] = useState("");
const [buscando, setBuscando] = useState(false);
const [erroBusca, setErroBusca] = useState("");

Estado

Valor inicial

Motivo

carregandoLista

true

A primeira tela ainda espera a lista.

erroLista

""

Ainda não existe erro de listagem.

buscando

false

Ninguém pesquisou ainda.

erroBusca

""

Ainda não existe erro de pesquisa.

Se usássemos um único booleano para lista e busca, a conclusão de uma operação poderia apagar o indicador da outra. Separar os estados preserva a independência dos fluxos.

O bloco finally

try {
  // Consulta e tratamento de resposta.
} catch (error) {
  // Mensagem em caso de falha.
} finally {
  setBuscando(false);
}

finally executa ao sair do try/catch, com sucesso ou falha. É o lugar adequado para encerrar o indicador da pesquisa. Não significa que a operação teve sucesso.

Como ler error.message com TypeScript?

Não presuma que todo valor lançado é um objeto Error:

const mensagem = error instanceof Error
  ? error.message
  : "Não foi possível concluir a operação.";

instanceof Error verifica o tipo em execução. O operador ternário escolhe a primeira mensagem se a condição for verdadeira e a segunda caso contrário. Esse refinamento de tipo é uma forma de narrowing no TypeScript.

Pratique: se a busca falhar, qual estado deverá terminar como false? Qual estado deverá receber a mensagem?

<ul>
  {usuarios.map((u) => (
    <li key={u.id}>
      {u.id} - {u.login}
      <img src={u.avatar_url} alt={`Avatar de ${u.login}`} width={30} height={30} />
    </li>
  ))}
</ul>

Leia uma repetição de cada vez

map percorre o array. Em cada execução da função, u representa um usuário. A função devolve um elemento li. O resultado do map é um novo array contendo os elementos a apresentar.

Você pode trocar u por usuario; o nome curto não possui comportamento especial.

Por que os parênteses depois da seta?

(u) => (<li key={u.id}>{u.login}</li>)

Essa forma retorna a expressão diretamente. Com chaves, é necessário escrever return:

(u) => {
  return <li key={u.id}>{u.login}</li>;
}

Se esquecer return na segunda forma, não produzirá os elementos esperados.

O papel de key

key={u.id} identifica cada item entre seus irmãos. Não é uma posição visual nem uma propriedade HTML comum. Use a identidade estável fornecida pelos dados; não gere uma chave aleatória a cada renderização. Consulte renderização de listas no React.

O que acontece com o array vazio?

[].map(...) produz outro array vazio. Não haverá itens até que os dados cheguem. Isso explica por que iniciar com [] permite renderizar antes da resposta sem chamar métodos sobre undefined.

Imagens e acessibilidade

src recebe a URL do avatar. alt descreve a imagem. width e height reservam dimensões. Os avatares serão carregados pelo navegador da aplicação, não pelo exportador do codelab: aqui, a tag está dentro de um bloco de código.

O trecho {" "} do exemplo inicial insere um espaço no texto JSX. Não interfere na requisição nem nos estados.

<label htmlFor="login-github">Login do GitHub</label>
<input
  id="login-github"
  type="text"
  value={digitado}
  onChange={(event) => setDigitado(event.target.value)}
  placeholder="Exemplo: octocat"
/>

label e input precisam estar associados

htmlFor="login-github" aponta para id="login-github". No exemplo inicial, htmlFor estava vazio. Agora, clicar no rótulo direciona o foco para o campo.

O placeholder é apenas uma dica e desaparece durante a digitação; ele não substitui o rótulo.

Como ler onChange

  1. A pessoa altera o campo.
  2. O navegador informa o evento.
  3. event.target identifica o elemento que originou a alteração.
  4. .value fornece o texto atual.
  5. setDigitado solicita atualização do estado.
  6. value={digitado} mantém o campo associado ao estado.

Essa ligação caracteriza um campo controlado. Apenas adicionar onChange, sem value, não torna o campo controlado pelo estado React.

Um erro para reconhecer

// Não use:
onChange={setDigitado(event.target.value)}

Precisamos fornecer uma função para ser executada quando o evento ocorrer, não executar a atualização durante a renderização.

Pratique: se a pessoa digitar ana, qual estado muda? Isso já deve mudar user?

Antes de acessar a rede:

const login = digitado.trim();

if (login === "") {
  setUser("");
  setErroBusca("Digite um login antes de pesquisar.");
  return;
}

trim() produz um texto sem espaços no início e no fim. Não remove espaços internos nem altera digitado automaticamente.

return encerra a função. Nesse caso, nenhuma requisição é necessária.

Montando o endereço

const url = `https://api.github.com/users/${encodeURIComponent(login)}`;

Trecho

Significado

Crases

Delimitam uma template string.

${...}

Insere o resultado de uma expressão no texto.

login

Contém o valor normalizado com trim.

encodeURIComponent

Codifica caracteres para que componham um segmento do endereço.

Se login for octocat, o endereço final será https://api.github.com/users/octocat.

Codificar não valida a existência de uma conta e não transforma um nome completo em login. A API ainda poderá recusar ou não encontrar o valor informado.

Array ou objeto?

Na pesquisa individual:

const data: TipoUserGit = await response.json();
setUser(data.login);

Não há [] no tipo porque o endpoint individual retorna um objeto. setUser(data) seria incompatível com nosso estado de texto. setUser(data.login) guarda somente o campo desejado.

Pratique: por que usar /users/ana é diferente de filtrar localmente o array usuarios?

O botão do código original usa onClick={buscador}. Isso é uma passagem válida de função, sem executá-la durante a renderização.

Vamos evoluir para um formulário, mantendo a mesma intenção e permitindo o envio com Enter:

import { useEffect, useState, type FormEvent } from "react";

FormEvent é importado como tipo porque descreve o evento, não uma função executada no navegador.

const buscador = async (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  // Validação e consulta serão inseridas aqui.
};

HTMLFormElement informa qual elemento está associado a esse evento. preventDefault() impede a navegação tradicional do formulário; o React continuará controlando a atualização da tela.

<form onSubmit={buscador}>
  <label htmlFor="login-github">Login do GitHub</label>
  <input
    id="login-github"
    value={digitado}
    onChange={(event) => setDigitado(event.target.value)}
  />
  <button type="submit">Pesquisar</button>
</form>

Não mantenha também onClick={buscador} nesse botão: centralize o fluxo no onSubmit. Isso evita misturar o tipo de evento de clique com o de formulário.

Impedindo pesquisas simultâneas na interface

Na versão final:

Esse desenho limita a pesquisa a uma operação por vez pelo formulário. Não é uma solução geral para autocomplete ou múltiplas buscas concorrentes; esses cenários exigem controle adicional de cancelamento ou da ordem das respostas.

A pessoa pode sair de Home antes de a resposta da lista chegar. Vamos associar o carregamento a um controlador:

useEffect(() => {
  const controller = new AbortController();

  async function loadingData() {
    // O fetch receberá { signal: controller.signal }.
  }

  loadingData();

  return () => {
    controller.abort();
  };
}, []);

AbortController é uma API do navegador, não um hook. signal informa à operação se houve cancelamento. abort() solicita o cancelamento. A função retornada pelo efeito é sua limpeza.

Na requisição:

const response = await fetch("https://api.github.com/users", {
  signal: controller.signal,
});

Depois de uma espera, verificaremos controller.signal.aborted antes de alterar os estados. No catch, cancelamentos não serão apresentados como falhas ao usuário. No finally, também evitaremos atualizar um carregamento cancelado.

Por que posso ver duas requisições no desenvolvimento?

Com o teste adicional de efeitos do StrictMode, você pode observar uma tentativa cancelada e outra válida. A limpeza torna o fluxo preparado para esse cenário. Não remova StrictMode apenas para esconder o teste.

Cancelar no navegador não garante que o servidor deixou de receber a requisição nem que ela deixou de contar para os limites da API.

Escopo desta limpeza

O controlador pertence à lista iniciada pelo efeito. Ele não cancela automaticamente a busca individual do formulário. Nesta versão didática, a busca poderá terminar mesmo após a saída da página; seu resultado não será reaproveitado pela próxima montagem de Home. Cancelar também a busca é uma evolução separada, que pode guardar outro controlador em uma referência.

Esta linha informa ao TypeScript o formato esperado:

const data: TipoUserGit[] = await response.json();

Ela não inspeciona automaticamente todos os elementos recebidos. As anotações de tipo não viram validação dos dados no navegador.

Para manter o primeiro contato próximo do código inicial, a versão completa usa o contrato esperado do GitHub. Em uma aplicação que precise validar respostas externas, comece recebendo unknown e confira a estrutura.

Extensão opcional: validar um usuário

function ehUsuarioGit(valor: unknown): valor is TipoUserGit {
  return (
    typeof valor === "object" &&
    valor !== null &&
    "login" in valor &&
    typeof valor.login === "string" &&
    "id" in valor &&
    typeof valor.id === "number" &&
    "avatar_url" in valor &&
    typeof valor.avatar_url === "string"
  );
}

unknown significa "ainda não sabemos o tipo". typeof verifica a categoria do valor. in verifica a presença da propriedade. && exige que todas as condições sejam satisfeitas. A ordem protege o acesso às propriedades.

valor is TipoUserGit é um predicado de tipo: quando a função retorna verdadeiro, o TypeScript pode tratar o argumento como TipoUserGit naquele caminho.

Para a lista, substitua a leitura tipada por:

const data: unknown = await response.json();

if (!Array.isArray(data) || !data.every(ehUsuarioGit)) {
  throw new Error("A API retornou uma lista com formato inesperado.");
}

setUsuarios(data);

Array.isArray verifica se é um array. every exige que todos os elementos passem no teste. Diferentemente de map, ele devolve um booleano.

Esse validador confere os tipos mínimos usados aqui. Ele não verifica se a URL do avatar existe nem se o identificador pertence realmente à conta informada.

Substitua o conteúdo de src/routes/Home/index.tsx pelo código abaixo. Não altere main.tsx nem App.tsx.

As classes de apresentação usam Tailwind, caso ele já esteja configurado no projeto. Sem Tailwind, a lógica funciona, mas essas classes não produzirão o estilo esperado. Nenhuma instalação adicional é necessária para fetch ou AbortController em navegadores modernos.

import { useEffect, useState, type FormEvent } from "react";

// Contrato dos campos utilizados nesta prática.
type TipoUserGit = {
  login: string;
  id: number;
  avatar_url: string;
};

export default function Home() {
  // Fluxo 1: lista carregada na entrada da página.
  const [usuarios, setUsuarios] = useState<TipoUserGit[]>([]);
  const [carregandoLista, setCarregandoLista] = useState(true);
  const [erroLista, setErroLista] = useState("");

  // Fluxo 2: texto digitado e resultado da pesquisa individual.
  const [user, setUser] = useState<string>("");
  const [digitado, setDigitado] = useState<string>("");
  const [buscando, setBuscando] = useState(false);
  const [erroBusca, setErroBusca] = useState("");

  useEffect(() => {
    // Cada configuração do efeito possui seu próprio controlador.
    const controller = new AbortController();

    async function loadingData() {
      try {
        const response = await fetch("https://api.github.com/users", {
          signal: controller.signal,
        });

        // Uma resposta HTTP de erro também precisa ser tratada.
        if (!response.ok) {
          throw new Error(
            `O carregamento da lista falhou. HTTP ${response.status}.`,
          );
        }

        // Tipagem estática: não substitui validação em execução.
        const data: TipoUserGit[] = await response.json();

        if (!controller.signal.aborted) {
          setUsuarios(data);
          setErroLista("");
        }
      } catch (error) {
        // Sair da página não deve aparecer como erro de carregamento.
        if (controller.signal.aborted) return;

        console.error(error);
        setErroLista(
          error instanceof Error
            ? error.message
            : "Não foi possível carregar os usuários.",
        );
      } finally {
        if (!controller.signal.aborted) {
          setCarregandoLista(false);
        }
      }
    }

    // Declarar a função acima não a executa. Esta chamada inicia o trabalho.
    loadingData();

    return () => {
      controller.abort();
    };
  }, []);

  const buscador = async (event: FormEvent<HTMLFormElement>) => {
    // Impede o envio tradicional do formulário e a navegação da página.
    event.preventDefault();

    // O formulário aceita uma pesquisa por vez.
    if (buscando) return;

    const login = digitado.trim();

    if (login === "") {
      setUser("");
      setErroBusca("Digite um login antes de pesquisar.");
      return;
    }

    // Remove o resultado anterior para não confundi-lo com a nova consulta.
    setUser("");
    setErroBusca("");
    setBuscando(true);

    try {
      // O fetch fica DENTRO do try, inclusive para capturar falhas de rede.
      const response = await fetch(
        `https://api.github.com/users/${encodeURIComponent(login)}`,
      );

      if (response.status === 404) {
        throw new Error(`O usuário "${login}" não foi encontrado.`);
      }

      if (!response.ok) {
        throw new Error(`A pesquisa falhou. HTTP ${response.status}.`);
      }

      // Aqui esperamos um objeto, não um array.
      const data: TipoUserGit = await response.json();
      setUser(data.login);
    } catch (error) {
      console.error(error);
      setErroBusca(
        error instanceof Error
          ? error.message
          : "Não foi possível pesquisar esse usuário.",
      );
    } finally {
      // Encerra a espera tanto em caso de sucesso quanto de falha.
      setBuscando(false);
    }
  };

  return (
    <main className="mx-auto max-w-4xl space-y-8 px-4 py-10">
      <header>
        <h1 className="text-3xl font-bold">Usuários do GitHub</h1>
        <p className="mt-2">Consulte a lista pública e pesquise um login exato.</p>
      </header>

      <section aria-labelledby="titulo-busca" className="rounded-xl border p-5">
        <h2 id="titulo-busca" className="mb-4 text-xl font-semibold">
          Pesquisar usuário
        </h2>

        <form onSubmit={buscador} aria-busy={buscando}>
          <label htmlFor="login-github" className="mb-2 block font-medium">
            Login do GitHub
          </label>

          <div className="flex flex-col gap-3 sm:flex-row">
            <input
              id="login-github"
              name="login"
              type="text"
              value={digitado}
              onChange={(event) => setDigitado(event.target.value)}
              placeholder="Exemplo: octocat"
              autoComplete="off"
              spellCheck={false}
              disabled={buscando}
              aria-describedby="ajuda-login"
              className="min-w-0 flex-1 rounded-lg border px-3 py-2"
            />

            <button
              type="submit"
              disabled={buscando}
              className="rounded-lg bg-slate-900 px-5 py-2 text-white disabled:opacity-60"
            >
              {buscando ? "Pesquisando..." : "Pesquisar"}
            </button>
          </div>

          <p id="ajuda-login" className="mt-2 text-sm">
            Informe o login, não o nome completo. Você também pode pressionar Enter.
          </p>
        </form>

        {erroBusca && (
          <p role="alert" className="mt-4 text-red-700">{erroBusca}</p>
        )}

        <div role="status" className="mt-4">
          {buscando && <p>Consultando o GitHub...</p>}
          {user && <p>Usuário encontrado: <strong>{user}</strong></p>}
        </div>
      </section>

      <section aria-labelledby="titulo-lista" aria-busy={carregandoLista}>
        <h2 id="titulo-lista" className="text-xl font-semibold">
          Lista inicial de usuários
        </h2>

        {carregandoLista && <p role="status">Carregando usuários...</p>}
        {erroLista && <p role="alert" className="mt-3 text-red-700">{erroLista}</p>}

        {!carregandoLista && !erroLista && usuarios.length === 0 && (
          <p>A lista recebida está vazia.</p>
        )}

        <ul className="mt-4 grid gap-3 sm:grid-cols-2">
          {usuarios.map((u) => (
            <li key={u.id} className="flex items-center gap-3 rounded-lg border p-3">
              <img
                src={u.avatar_url}
                alt={`Avatar de ${u.login}`}
                width={40}
                height={40}
                loading="lazy"
                className="rounded-full"
              />
              <span>{u.id} - {u.login}</span>
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}

Conferência após colar

  1. Salve o arquivo.
  2. Abra / no navegador.
  3. Observe a mensagem de carregamento.
  4. Aguarde a lista.
  5. Digite um login conhecido, como octocat.
  6. Envie com Enter.
  7. Confira o resultado acima da lista.
  8. Verifique que a listagem inicial não foi substituída pela pesquisa.

O JSX completo possui condições que selecionam o que será apresentado.

{buscando ? "Pesquisando..." : "Pesquisar"}

O ternário escolhe entre dois textos. O primeiro corresponde a verdadeiro; o segundo, a falso.

{erroBusca && <p role="alert">{erroBusca}</p>}

Quando a mensagem é uma string não vazia, o parágrafo é apresentado. Quando é "", não aparece texto de erro.

{!carregandoLista && !erroLista && usuarios.length === 0 && (
  <p>A lista recebida está vazia.</p>
)}

Aqui, três condições precisam ser verdadeiras:

  1. O carregamento terminou.
  2. Não existe mensagem de erro.
  3. O array não possui elementos.

Sem as duas primeiras verificações, uma tela aguardando resposta poderia dizer "lista vazia" prematuramente.

Atributos de acessibilidade utilizados

Atributo

Finalidade

aria-labelledby

Associa a seção ao título indicado por id.

aria-describedby

Associa o campo ao texto de ajuda.

aria-busy

Informa que aquela região está ocupada.

role="status"

Identifica atualizações informativas.

role="alert"

Identifica uma mensagem de erro que merece atenção.

As mensagens não dependem apenas de cor. Leia-as com o conteúdo da interface, não como substitutas de rótulos visíveis.

Abra as ferramentas do navegador com F12. Use Network/Rede para acompanhar as requisições e Console para investigar falhas.

Teste

Ação

Resultado esperado

Listagem

Entrar em /.

Indicador e depois usuários, ou mensagem de falha.

Digitação

Digitar sem enviar.

Nenhuma nova consulta individual.

Busca válida

Pesquisar um login existente.

Login retornado aparece.

Campo vazio

Enviar sem texto.

Mensagem local, sem nova requisição.

Espaços

Enviar somente espaços.

Mesmo tratamento do vazio.

Espaços nas pontas

Pesquisar octocat.

Consulta feita com octocat.

Login inexistente

Enviar um login que não exista.

Se o servidor devolver 404, mensagem específica.

Rede desconectada

Ativar Offline na aba de rede e pesquisar.

Erro tratado; botão habilitado novamente ao terminar.

Enter

Enviar pelo teclado.

Mesmo fluxo do botão.

Espera

Usar limitação de velocidade na aba de rede.

Campo e botão desabilitados durante a busca.

Depois do teste Offline, restaure a conexão na ferramenta. Não deixe o navegador simulando falta de rede ao continuar.

O que observar na requisição

  1. Confira a URL: o login está no segmento correto?
  2. Confira o método: GET.
  3. Confira o status.
  4. Na resposta, identifique se o corpo é array ou objeto.
  5. Compare os campos com TipoUserGit.

Erros frequentes

Sintoma

Investigue

A lista nunca é solicitada

A chamada loadingData() foi esquecida?

Muitas consultas a cada tecla

O fetch foi colocado no corpo do componente ou num efeito dependente de digitado?

Falha de rede não aparece na tela

O fetch da busca está dentro do try? O catch atualiza o estado de erro?

map is not a function

O estado recebeu um objeto de erro ou uma resposta individual em vez de array?

O resultado antigo continua após uma falha

A nova pesquisa limpou user?

Campo não aceita digitação

Existe value sem um onChange que atualize o estado?

Página navega ao enviar

preventDefault() foi chamado?

HTTP 403 ou 429

Inspecione a resposta e os cabeçalhos; pode haver limitação de requisições.

Um cuidado especial em sala de aula

Consultas públicas sem autenticação têm limite primário de 60 requisições por hora por IP. Alunos na mesma rede podem compartilhar o IP público e consumir a mesma cota. Há também limites secundários; não tente contorná-los. Consulte a documentação de limites do GitHub.

Não coloque tokens pessoais no componente, no repositório ou em variáveis VITE_* pensando que ficarão secretos. Código e variáveis expostos ao front-end podem ser inspecionados. Esta prática usa somente consultas públicas sem token.

Verificação do projeto

npm run build

Executa o script de construção definido no projeto. Leia as mensagens de TypeScript e corrija os erros apontados.

Se existir um script lint no package.json, execute também:

npm run lint

Não desative regras de hooks apenas para fazer o aviso desaparecer. Confira a causa.

Exercício 1 — tipos

Explique a diferença entre TipoUserGit e TipoUserGit[]. Em qual requisição cada um é utilizado?

Exercício 2 — sequência

Coloque em ordem: interpretar o corpo, iniciar a requisição, atualizar o estado, verificar ok, apresentar a nova renderização.

Exercício 3 — duas ações diferentes

Explique por que digitar altera digitado, mas não deve executar a pesquisa neste exemplo.

Exercício 4 — diagnóstico

O colega escreveu fetch antes do try. Uma resposta 404 entra no tratamento, mas uma falha de rede não. Explique essa diferença e corrija a estrutura.

Exercício 5 — map

Crie um array contendo apenas os logins recebidos. Depois explique por que o array original permanece disponível.

Exercício 6 — filtro local

Crie uma variável usuariosFiltrados que contenha apenas os usuários da lista inicial cujo login inclua o texto digitado, sem diferenciar maiúsculas e minúsculas. Não faça nova requisição e não altere a pesquisa individual por enquanto.

Explique por que isso não pesquisa em todos os usuários do GitHub.

Exercício 7 — resultado completo

Evolua user para armazenar TipoUserGit | null. Apresente login, id e avatar do resultado da pesquisa. Atualize também a limpeza do resultado.

Exercício 8 — validação real

Use ehUsuarioGit, da etapa 17, para verificar a resposta individual antes de armazená-la.

Exercício 9 — independência dos fluxos

Simule falha na listagem e explique por que a pesquisa ainda possui seus próprios estados. Qual seria o problema de compartilhar um único erro entre ambas?

Prática integradora

Implemente os exercícios 6, 7 e 8. Mantenha o carregamento inicial no efeito e a busca individual no formulário. Teste teclado, campo vazio, resposta válida e falha de rede.

Não altere as rotas. Não transforme o efeito em uma função async. Não use um efeito adicional apenas para copiar um array filtrado para outro estado.

Respostas das verificações durante o estudo

Etapa

Resposta

2

/ seleciona uma página local; a URL da API solicita dados ao servidor.

4

"123" é string. Use 123 se o contrato exige number.

5

usuarios começa com []; user e digitado, com "".

7

O efeito repetiria a busca quando o texto mudasse, além de executar inicialmente.

8

A continuação suspensa é a de loadingData.

9

response.ok informa sucesso HTTP; data.login é um campo do corpo interpretado.

11

buscando termina falso; erroBusca recebe a mensagem.

13

Muda digitado. user representa a resposta, não a digitação.

14

A consulta remota busca aquele login; o filtro local só examina os usuários já recebidos.

Exercícios 1 a 4

  1. TipoUserGit representa um objeto, usado na pesquisa individual. TipoUserGit[] representa uma lista, usada no carregamento inicial.
  2. Iniciar requisição, verificar ok, interpretar o corpo, atualizar o estado, apresentar a nova renderização.
  3. A digitação atualiza o campo controlado. O envio do formulário é o evento escolhido para iniciar a pesquisa.
  4. Um 404 devolve uma resposta que pode ser inspecionada no try posterior. Uma falha de rede rejeita o await que estava fora dele. Coloque await fetch(...), a verificação HTTP e a interpretação do corpo dentro do try.

Exercício 5

const logins = usuarios.map((usuario) => usuario.login);

map cria um novo array com os retornos da função. Esse callback apenas lê cada objeto; não modifica o array original.

Exercício 6

Dentro de Home, antes do return:

const termo = digitado.trim().toLowerCase();
const usuariosFiltrados = usuarios.filter((usuario) =>
  usuario.login.toLowerCase().includes(termo),
);

filter mantém os itens cujo teste devolve verdadeiro. toLowerCase normaliza letras. includes verifica a presença de uma sequência de caracteres.

Para visualizar o filtro, troque usuarios.map por usuariosFiltrados.map. Ajuste a mensagem de vazio para considerar usuariosFiltrados.length e diga "Nenhum usuário da lista corresponde ao filtro".

Não crie estado extra para esse resultado: ele é calculado a partir de usuarios e digitado. Somente a página já carregada será filtrada.

Exercício 7

Substitua o estado:

const [user, setUser] = useState<TipoUserGit | null>(null);

O símbolo | representa união: o estado aceita um usuário ou a ausência dele, representada por null.

Substitua as duas limpezas setUser("") por setUser(null). Na resposta bem-sucedida, use setUser(data).

Substitua o trecho do resultado:

{user && (
  <article>
    <h3>{user.login}</h3>
    <p>Identificador: {user.id}</p>
    <img
      src={user.avatar_url}
      alt={`Avatar de ${user.login}`}
      width={80}
      height={80}
    />
  </article>
)}

Não mantenha {user} sozinho no JSX, pois agora ele é um objeto, não um texto.

Exercício 8

Coloque ehUsuarioGit acima de Home. Dentro do try da pesquisa, substitua a leitura e a atualização por:

const data: unknown = await response.json();

if (!ehUsuarioGit(data)) {
  throw new Error("O usuário recebido tem formato inesperado.");
}

setUser(data);

Esse trecho pressupõe o estado em objeto do exercício 7. Se manteve o estado original de texto, a última linha deve ser setUser(data.login).

Exercício 9

Lista e pesquisa podem terminar em momentos diferentes. Um erro compartilhado poderia ser apagado pela operação errada, ou aparecer na região errada da interface. Estados independentes deixam explícita a origem da mensagem.

Checklist

Glossário de consulta rápida

Termo

Significado nesta prática

Estado

Informação preservada pelo React entre renderizações.

Hook

Função que permite utilizar recursos do React.

Efeito

Sincronização do componente com um sistema externo.

Callback

Função entregue para outra parte do programa executar.

Endpoint

Endereço de uma operação de API.

Requisição

Solicitação enviada ao servidor.

Response

Objeto que representa a resposta HTTP.

JSON

Formato textual usado para transportar dados.

Promise

Representação de uma conclusão futura.

async

Declara uma função que devolve Promise.

await

Aguarda um resultado sem bloquear toda a página.

throw

Lança um erro e interrompe o caminho normal.

catch

Trata uma falha do bloco protegido.

finally

Executa ao sair do tratamento, com sucesso ou falha.

Montagem

Entrada de uma instância do componente na interface.

Limpeza

Função que desfaz ou interrompe trabalho do efeito.

Referências oficiais e documentação técnica

Ao concluir, explique sem ler o código: o que inicia cada consulta, qual formato chega da API, qual estado é atualizado e como essa atualização aparece na tela.