Vamos transformar o componente Home em uma tela que consulta usuários públicos do GitHub. O trabalho tem duas partes independentes:
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.
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.
Você precisa ter:
my-app com Vite, React e TypeScript funcionando;/ apresentando o componente Home;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.
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 |
| Configura |
| Mantém |
| Recebe o código desta prática. |
| Permanece como está. |
| Permanece como está. |
| Continua configurado em |
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.
Exemplo | Quem atende? | Para que serve? |
| O roteador da aplicação | Apresenta |
| 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.
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 |
| Uma lista de usuários, representada por um array. |
| 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 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.
É 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.
Arquivo | API pela rede |
Os dados fazem parte do código da aplicação. | Os dados vêm de outro sistema. |
Um | 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 |
| Declara um nome para um tipo. |
| Nome escolhido para esse contrato. |
| Descreve as propriedades do objeto. |
| O login é texto. |
| O identificador é numérico. |
| 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.
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.
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.
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 |
| 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.
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).
Alterar uma variável comum não solicita ao React que atualize a interface. Já o estado participa do processo de renderização.
const [user, setUser] = useState<string>("");
const [digitado, setDigitado] = useState<string>("");
Estado | O que guarda | Quem o altera |
| Lista inicial | Resposta da listagem. |
| Conteúdo atual do campo | Evento |
| 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 |
Envio do formulário | A lista permanece. |
|
Resposta bem-sucedida | Atualiza | Atualiza |
Falha | Atualiza | Atualiza |
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 |
| Registra a sincronização com um sistema externo. |
| Função de configuração do efeito. |
| Função interna que realiza o trabalho assíncrono. |
| 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.
Forma | Comportamento |
Sem segundo argumento | Executa após cada confirmação de renderização. |
| Não repete por mudanças de dependências. |
| Repete quando |
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.
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.
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.
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();
}, []);
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();
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.
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 |
| Resposta HTTP | Conferir sucesso antes de continuar. |
| Valor interpretado do corpo | Atualizar o estado. |
Não escreva setUsuarios(response): o estado espera um array, não o envelope da resposta HTTP.
// Não use:
const data = response.json();
setUsuarios(data);
Sem o segundo await, data representa uma Promise, não o array pronto.
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);
}
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".
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.
Falha | Onde aparece | Exemplo de tratamento |
Comunicação de rede | Durante | Avisar que a consulta falhou. |
HTTP sem sucesso | Na verificação de | Lançar uma mensagem apropriada ao status. |
Corpo inválido para JSON | Durante | Capturar a falha de interpretação. |
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.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 |
|
| A primeira tela ainda espera a lista. |
|
| Ainda não existe erro de listagem. |
|
| Ninguém pesquisou ainda. |
|
| 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.
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.
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>
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.
(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.
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.
[].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.
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"
/>
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.
event.target identifica o elemento que originou a alteração..value fornece o texto atual.setDigitado solicita atualização do estado.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.
// 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.
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. |
| Contém o valor normalizado com |
| 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.
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.
Na versão final:
buscando já for verdadeiro;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.
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.
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.
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>
);
}
/ no navegador.octocat.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:
Sem as duas primeiras verificações, uma tela aguardando resposta poderia dizer "lista vazia" prematuramente.
Atributo | Finalidade |
| Associa a seção ao título indicado por |
| Associa o campo ao texto de ajuda. |
| Informa que aquela região está ocupada. |
| Identifica atualizações informativas. |
| 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 | Consulta feita com |
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.
GET.TipoUserGit.Sintoma | Investigue |
A lista nunca é solicitada | A chamada |
Muitas consultas a cada tecla | O fetch foi colocado no corpo do componente ou num efeito dependente de |
Falha de rede não aparece na tela | O fetch da busca está dentro do try? O catch atualiza o estado de erro? |
| 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 |
Campo não aceita digitação | Existe |
Página navega ao enviar |
|
HTTP 403 ou 429 | Inspecione a resposta e os cabeçalhos; pode haver limitação de requisições. |
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.
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.
Explique a diferença entre TipoUserGit e TipoUserGit[]. Em qual requisição cada um é utilizado?
Coloque em ordem: interpretar o corpo, iniciar a requisição, atualizar o estado, verificar ok, apresentar a nova renderização.
Explique por que digitar altera digitado, mas não deve executar a pesquisa neste exemplo.
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.
Crie um array contendo apenas os logins recebidos. Depois explique por que o array original permanece disponível.
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.
Evolua user para armazenar TipoUserGit | null. Apresente login, id e avatar do resultado da pesquisa. Atualize também a limpeza do resultado.
Use ehUsuarioGit, da etapa 17, para verificar a resposta individual antes de armazená-la.
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?
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.
Etapa | Resposta |
2 |
|
4 |
|
5 |
|
7 | O efeito repetiria a busca quando o texto mudasse, além de executar inicialmente. |
8 | A continuação suspensa é a de |
9 |
|
11 |
|
13 | Muda |
14 | A consulta remota busca aquele login; o filtro local só examina os usuários já recebidos. |
TipoUserGit representa um objeto, usado na pesquisa individual. TipoUserGit[] representa uma lista, usada no carregamento inicial.ok, interpretar o corpo, atualizar o estado, apresentar a nova renderização.await fetch(...), a verificação HTTP e a interpretação do corpo dentro do try.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.
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.
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.
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).
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.
src/routes/Home/index.tsx.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. |
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.