Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Consumir API no React: fetch, carregando e erro

Como buscar dados de uma API dentro do componente, tratar carregando e erro, cancelar requisição antiga e lidar com respostas que chegam fora de ordem.

Rodolfo Mori15 min de leitura

Você troca o filtro de “Básico” para “Avançado”, o título muda, mas a lista termina mostrando as turmas básicas. Nenhuma mensagem de erro aparece: a resposta antiga apenas chegou depois e sobrescreveu a escolha nova.

Nesta aula, você vai da primeira busca a um painel que representa espera, vazio, erro e sucesso, rejeita respostas HTTP defeituosas e evita corridas entre requisições. No fim, a lógica repetida vira um hook reutilizável.

Uma API é um contrato por onde programas trocam dados. O navegador com React é o cliente, que pede; o programa que atende é o servidor. A rede é o caminho entre eles, e HTTP é o conjunto de regras da conversa, incluindo o status, número como 200 ou 404 que resume o resultado.

fetch é a função do navegador que inicia a requisição HTTP e devolve uma Promise, objeto que representará um resultado futuro. A resposta pode trazer JSON, formato de texto para dados estruturados. useState guarda valores entre renderizações, execuções do componente que descrevem a tela; useEffect sincroniza o componente com algo externo, como a rede. Ambos são hooks, funções do React cujo nome começa com use.

Pense numa comanda: componente é o cliente, comanda é a requisição, garçom é a rede, cozinha é o servidor e prato é a resposta em JSON. O status informa se o pedido foi atendido. AbortController avisa que o cliente não quer mais esperar pela comanda antiga. O limite da analogia: abortar o fetch não desfaz trabalho que o servidor já realizou nem apaga bytes já recebidos; ele interrompe o que ainda pode ser interrompido no lado cliente.

Os exemplos usam a Escola Aurora, uma escola de idiomas fictícia criada para esta aula. A API local introduz atrasos diferentes para tornar visível a corrida que, numa aplicação real, pode acontecer rápido demais para notar.

js
// api.mjs — a API da Escola Aurora
import express from 'express';

const turmas = {
  basico: [
    { id: 'B-101', nome: 'Inglês Básico — seg/qua 08h', turno: 'manha', vagas: 4 },
    { id: 'B-102', nome: 'Inglês Básico — ter/qui 19h', turno: 'noite', vagas: 0 },
  ],
  intermediario: [
    { id: 'I-201', nome: 'Inglês Intermediário — seg/qua 20h', turno: 'noite', vagas: 2 },
  ],
  avancado: [
    { id: 'A-301', nome: 'Conversação Avançada — sáb 09h', turno: 'manha', vagas: 6 },
    { id: 'A-302', nome: 'Conversação Avançada — qui 19h', turno: 'noite', vagas: 3 },
  ],
  corporativo: [],
};

const atraso = { basico: 900, intermediario: 400, avancado: 150, corporativo: 200 };

const app = express();

app.get('/turmas', async (req, res) => {
  const { nivel = '', turno } = req.query;

  if (!turmas[nivel]) {
    return res.status(404).json({ erro: `nivel "${nivel}" nao existe` });
  }

  await new Promise((r) => setTimeout(r, atraso[nivel]));

  const lista = turno ? turmas[nivel].filter((t) => t.turno === turno) : turmas[nivel];
  res.json(lista);
});

app.listen(4517, () => console.log('API da Escola Aurora em http://localhost:4517'));

turmas é o conjunto de dados; atraso define quanto cada nível espera. Express cria o servidor e a rota HTTP GET /turmas. Nela, req.querynivel e turno da URL, o status 404 sinaliza nível inexistente, filter limita o turno e res.json envia a lista como JSON. listen(4517) abre a porta local 4517.

No terminal, a janela de texto usada para executar comandos, a linha abaixo inicia esse servidor:

bash
node api.mjs
API da Escola Aurora em http://localhost:4517

Quais estados uma tela de API precisa mostrar?

No mínimo: carregando, erro, vazio e sucesso. Eles distinguem uma requisição em andamento de uma resposta sem itens ou de uma falha verdadeira:

  • carregando — o pedido saiu e ninguém respondeu ainda;
  • erro — a resposta não veio, ou veio com defeito;
  • vazio — a resposta veio certinha, e é uma lista sem nada dentro;
  • sucesso — tem dado para desenhar.

Quem esquece um deles escreve a tela que pisca em branco, ou a tela que mostra “nenhuma turma” enquanto a requisição ainda está no ar. Três useState e três return antecipados resolvem:

Experimente você mesmo

A sequência esperada é carregando, lista do básico, carregando e vazio. Antes de ler a saída, preveja o texto e a quantidade de linhas de cada etapa. Execute o painel e compare com sua anotação.

jsx
const API = 'http://localhost:4517';

function PainelDeTurmas({ nivel }) {
  const [turmas, setTurmas] = useState([]);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState(null);

  useEffect(() => {
    setCarregando(true);
    setErro(null);

    fetch(`${API}/turmas?nivel=${nivel}`)
      .then((resposta) => resposta.json())
      .then((dados) => setTurmas(dados))
      .catch((e) => setErro(e.message))
      .finally(() => setCarregando(false));
  }, [nivel]);

  if (carregando) return <p>Carregando turmas…</p>;
  if (erro) return <p>Não deu para carregar as turmas: {erro}</p>;
  if (turmas.length === 0) return <p>Nenhuma turma aberta neste nível.</p>;

  return (
    <ul>
      {turmas.map((turma) => (
        <li key={turma.id}>
          {turma.nome}{turma.vagas} vagas
        </li>
      ))}
    </ul>
  );
}

Os três useState guardam dados, loading (carregando) e erro. O efeito roda ao montar e sempre que nivel muda; fetch inicia a busca, os .then leem o JSON e salvam as turmas, .catch recebe uma Promise rejeitada e .finally encerra a espera. Os return escolhem uma única tela antes de chegar à lista.

Esta é a versão mínima para enxergar os estados. Ela ainda não cancela a busca anterior nem verifica resposta.ok: fetch normalmente não rejeita a Promise só porque o servidor respondeu com status 4xx ou 5xx. Corrigiremos ambos.

Montei esse componente com nivel="basico", imprimi o HTML gerado logo depois da montagem e depois da resposta, e então troquei para corporativo, que é um nível que existe na API e não tem turma aberta:

nivel=basico, ao montar <p>Carregando turmas…</p> nivel=basico, ja respondeu <ul><li>Inglês Básico — seg/qua 08h — 4 vagas</li><li>Inglês Básico — ter/qui 19h — 0 vagas</li></ul> nivel=corporativo <p>Nenhuma turma aberta neste nível.</p>

Começar carregando em true evita mostrar o vazio antes da primeira resposta. A ordem também importa: erro antes de vazio, vazio antes da lista. Revise esses return em renderização condicional e o key em listas com map e key.

Como usar async dentro do useEffect?

Mantenha a função do efeito síncrona e declare uma função async dentro dela. Assim, o retorno do efeito continua reservado para a limpeza.

async marca uma função que devolve Promise; await espera o resultado dentro dela sem travar a página. A tentação é colocar async direto no efeito:

jsx
useEffect(async () => {
  const resposta = await fetch(`${API}/turmas?nivel=${nivel}`);
  setTurmas(await resposta.json());
}, [nivel]);

O código até busca os dados, mas o React reclama alto no console — e a reclamação é longa porque ela vem com a solução escrita:

useEffect must not return anything besides a function, which is used for clean-up.

It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately:

useEffect(() => { async function fetchData() { // You can await here const response = await MyAPI.getData(someId); // … } fetchData(); }, [someId]); // Or [] if effect doesn’t need props or state

Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching

O retorno do efeito é reservado para a função de limpeza, chamada antes de repetir o efeito ou desmontar o componente. Como uma função async sempre devolve Promise, ela não pode ocupar esse lugar.

A forma correta é declarar a função assíncrona dentro do efeito e chamá-la na linha seguinte:

jsx
useEffect(() => {
  async function buscarTurmas() {
    const resposta = await fetch(`${API}/turmas?nivel=${nivel}`);
    setTurmas(await resposta.json());
  }

  buscarTurmas();
}, [nivel]);

buscarTurmas pode usar await porque ela é assíncrona. O efeito apenas chama essa função e continua sem devolver sua Promise; [nivel] manda repetir a busca quando a prop mudar.

Se async e await ainda soam nebulosos, a lição de async e await em JavaScript mostra a mecânica por trás, e a de Fetch API cobre o fetch fora do React.

Como cancelar uma requisição antiga com AbortController?

Crie um AbortController, passe seu signal ao fetch e chame abort() na limpeza. O sinal pode interromper trabalho pendente no cliente. Na analogia, a comanda é riscada na mesa, mas a cozinha pode já ter começado; o cancelamento não desfaz processamento do servidor nem resposta recebida.

jsx
useEffect(() => {
  const controlador = new AbortController();

  fetch(`${API}/turmas?nivel=${nivel}`, { signal: controlador.signal })
    .then((r) => r.json())
    .then((dados) => setTurmas(dados))
    .catch((e) => {
      if (e.name === 'AbortError') return;
      setErro(e.message);
    });

  return () => controlador.abort();
}, [nivel]);

O signal liga controlador e fetch; o return é a limpeza chamada antes do próximo efeito ou da desmontagem. Se o fetch ainda estiver pendente quando for abortado, sua Promise rejeita com AbortError. Esse cancelamento esperado não deve virar mensagem de falha.

Experimente você mesmo

Dentro de <StrictMode>, somente no ambiente de desenvolvimento, o React executa um ciclo extra de configuração, limpeza e nova configuração para testar o efeito. Abra os logs, preveja essa ordem e marque qual das duas respostas deve chegar ao estado; depois compare com a execução abaixo. ms() apenas mede o tempo desde a montagem.

jsx
useEffect(() => {
  const controlador = new AbortController();
  console.log(ms(), 'efeito rodou  ', nivel);

  fetch(`${API}/turmas?nivel=${nivel}`, { signal: controlador.signal })
    .then((r) => r.json())
    .then((dados) => {
      console.log(ms(), 'resposta usada', nivel);
      setTurmas(dados);
    })
    .catch((e) => {
      if (e.name === 'AbortError') console.log(ms(), 'abortado      ', nivel);
      else throw e;
    });

  return () => {
    console.log(ms(), 'limpeza rodou ', nivel);
    controlador.abort();
  };
}, [nivel]);
5ms efeito rodou avancado 12ms limpeza rodou avancado 12ms efeito rodou avancado 12ms abortado avancado 173ms resposta usada avancado tela: <p>2 turmas</p>

Duas chamadas de fetch saíram; a primeira foi abortada em 12 milissegundos e só a segunda virou estado. Vale olhar do outro lado do fio: pus um contador na rota /turmas da API — console.log('SERVIDOR recebeu pedido', ++n, req.query) — e comparei esse ciclo do StrictMode com a troca de filtro da próxima seção, onde o cancelamento chega bem mais tarde.

StrictMode (abort em 12ms): SERVIDOR recebeu pedido 1 {"nivel":"avancado"} troca de filtro (abort em 117ms): SERVIDOR recebeu pedido 1 {"nivel":"basico"} SERVIDOR recebeu pedido 2 {"nivel":"avancado"}

No teste do StrictMode, abort() agiu antes do envio e o servidor registrou um acesso. Na troca de filtro, os dois pedidos já tinham partido: cancelar evitou usar a resposta velha, não o trabalho iniciado no servidor. O ciclo diagnóstico duplo não ocorre em produção, mas a mesma limpeza protege a troca rápida de filtro.

Até aqui: o efeito abre a sincronização com a rede, e sua limpeza avisa que a busca anterior perdeu o interesse. O ciclo extra do StrictMode existe apenas no desenvolvimento e ajuda a revelar limpeza ausente.

Como impedir que uma resposta antiga sobrescreva a nova?

Cancele a busca anterior na limpeza do efeito. Sem isso, duas Promises podem terminar fora de ordem e a mais antiga gravar dados sobre a seleção atual.

Esse defeito se chama condição de corrida: o resultado depende de qual tarefa termina primeiro. A secretaria seleciona “Básico” (900 ms) e, 100 ms depois, “Avançado” (150 ms). As duas requisições ficam no ar e voltam fora de ordem.

Sem cancelamento nenhum, com um log na saída e outro na chegada de cada uma. O componente desta demonstração imprime o nível selecionado num <p> acima da lista, para você ver as duas informações na mesma tela:

jsx
useEffect(() => {
  console.log(ms(), 'pediu ', nivel);
  fetch(`${API}/turmas?nivel=${nivel}`)
    .then((r) => r.json())
    .then((dados) => {
      console.log(ms(), 'chegou', nivel, `(${dados.length} turmas)`);
      setTurmas(dados);
    });
}, [nivel]);
5ms pediu basico 115ms pediu avancado 273ms chegou avancado (2 turmas) 920ms chegou basico (2 turmas) tela final: <div><p>Nível selecionado: avancado</p><ul><li>Inglês Básico — seg/qua 08h</li><li>Inglês Básico — ter/qui 19h</li></ul></div>

Na última linha, o título diz avancado, mas a lista é do básico. A resposta nova chegou primeiro; depois, a antiga sobrescreveu o estado sem gerar erro. Os tempos confirmam a condição de corrida.

O mesmo teste, trocando só o efeito pela versão com AbortController:

5ms pediu basico 117ms pediu avancado 117ms abortou basico (AbortError) 277ms chegou avancado (2 turmas) tela final: <div><p>Nível selecionado: avancado</p><ul><li>Conversação Avançada — sáb 09h</li><li>Conversação Avançada — qui 19h</li></ul></div>

A limpeza rodou quando o filtro mudou. O cliente deixou de esperar e usar a resposta do básico; se o servidor já tinha começado, seu trabalho pode ter continuado. O efeito visível é a tela terminar coerente.

Outra opção é marcar a busca antiga com uma variável booleana na limpeza:

jsx
useEffect(() => {
  let atual = true;

  fetch(`${API}/turmas?nivel=${nivel}`)
    .then((r) => r.json())
    .then((dados) => {
      if (atual) setTurmas(dados);
    });

  return () => {
    atual = false;
  };
}, [nivel]);

Ela resolve o sintoma — a resposta velha chega e é ignorada — mas o download continua ocupando conexão e banda. Prefira o AbortController; guarde a booleana para os casos em que o cliente HTTP não aceita signal.

Qual é a diferença entre erro de rede e status HTTP?

Falha de rede impede uma resposta HTTP utilizável e normalmente rejeita a Promise. Já um status 404 ou 500 é uma resposta recebida: fetch não rejeita somente por causa desse status, então o código precisa verificar resposta.ok.

Primeiro, peça um nível inexistente:

js
const resposta = await fetch(`${API}/turmas?nivel=intermediarrio`);
console.log('resposta.ok    =', resposta.ok);
console.log('resposta.status=', resposta.status);
console.log('corpo          =', JSON.stringify(await resposta.json()));
resposta.ok = false resposta.status= 404 corpo = {"erro":"nivel \"intermediarrio\" nao existe"}

ok é true para status de 200 a 299; status guarda o número 404; resposta.json() interpreta o corpo JSON e devolve outra Promise.

Na analogia, o prato chegou, mas com a etiqueta 404. Para fetch, a entrega da resposta aconteceu; interpretar esse status é trabalho do seu código. Por isso o .then((r) => r.json()) do primeiro exemplo passa liso e entrega ao estado um objeto { erro: ... } no lugar da lista. O estrago aparece no render, e a mensagem é esta:

TypeError: turmas.map is not a function

Já quando a API está fora do ar, o fetch rejeita de verdade:

js
try {
  await fetch('http://localhost:4517/turmas?nivel=avancado');
} catch (e) {
  console.log(e.name + ': ' + e.message);
  console.log('  cause:', e.cause?.name, e.cause?.code);
  for (const x of e.cause?.errors ?? []) console.log('   ', x.code, x.address + ':' + x.port);
}
TypeError: fetch failed cause: AggregateError ECONNREFUSED ECONNREFUSED ::1:4517 ECONNREFUSED 127.0.0.1:4517

Tratar os dois casos custa uma linha de checagem e um try/catch. A função abaixo mora dentro do efeito, logo depois de criar o controlador:

jsx
async function buscar() {
  try {
    const resposta = await fetch(`${API}/turmas?nivel=${nivel}`, {
      signal: controlador.signal,
    });

    if (!resposta.ok) {
      throw new Error(`a secretaria respondeu ${resposta.status}`);
    }

    setTurmas(await resposta.json());
  } catch (e) {
    if (e.name === 'AbortError') return;
    setErro(e.message);
  } finally {
    if (!controlador.signal.aborted) setCarregando(false);
  }
}

O try agrupa a operação que pode falhar. Status fora da faixa de sucesso vira erro explícito; no sucesso, o JSON alimenta setTurmas. O catch ignora apenas o cancelamento esperado e guarda as outras mensagens. O finally executa nos dois caminhos, mas sua guarda evita que uma busca abortada desligue o loading da requisição mais nova.

nivel=avancado → <ul><li>Conversação Avançada — sáb 09h</li><li>Conversação Avançada — qui 19h</li></ul> nivel=intermediarrio → <p>Não deu para carregar as turmas: a secretaria respondeu 404</p> API fora do ar → <p>Não deu para carregar as turmas: fetch failed</p>

Repare no finally: ele só desliga o “carregando” se a requisição não foi abortada. Parece implicância, e não é. Rodei o mesmo componente com e sem essa guarda, trocando de basico para avancado depois de 100 ms e imprimindo a tela a cada 50 ms. Sem a guarda:

172ms <p>Nenhuma turma aberta neste nível.</p> 223ms <p>Nenhuma turma aberta neste nível.</p> 285ms <ul><li>Conversação Avançada — sáb 09h</li><li>Conversação Avançada — qui 19h</li></ul>

Com a guarda:

170ms <p>Carregando turmas…</p> 223ms <p>Carregando turmas…</p> 274ms <ul><li>Conversação Avançada — sáb 09h</li><li>Conversação Avançada — qui 19h</li></ul>

Sem a guarda, o finally da busca abortada desliga o loading enquanto a nova ainda está no ar. Nas cinco execuções, a mensagem vazia ficou visível por 152 a 159 ms. Com a guarda, “Carregando turmas…” permanece até a resposta atual.

Até aqui: erro de rede chega pela rejeição da Promise; erro HTTP exige olhar ok ou status; cancelamento esperado não vira erro de interface. A guarda no finally mantém o loading da requisição atual.

Como buscar de novo quando o filtro muda?

O array no fim do useEffect lista as dependências, valores que React observa para decidir se repete a sincronização. Inclua primitivos usados pela busca, como nivel e turno; evite um objeto novo criado em cada renderização:

jsx
function PainelDeTurmas({ nivel }) {
  const [turmas, setTurmas] = useState([]);
  const filtros = { nivel, turno: 'noite' };

  useEffect(() => {
    fetch(`${API}/turmas?nivel=${filtros.nivel}`)
      .then((r) => r.json())
      .then(setTurmas);
  }, [filtros]);

  return <p>{turmas.length} turmas</p>;
}

React compara dependências por identidade: cada render cria outro objeto filtros, mesmo com campos iguais. O efeito busca, setTurmas renderiza, nasce outro objeto e o ciclo recomeça. O contador mostra os três primeiros pedidos e o total após três segundos sem interação:

5ms pedido 1 219ms pedido 2 375ms pedido 3 em 3 segundos: 20 pedidos para uma tela parada

Foram vinte requisições em três segundos. A correção é depender dos valores primitivos, não do objeto que os embrulha:

jsx
function PainelDeTurmas({ nivel, turno }) {
  const [turmas, setTurmas] = useState([]);

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

    fetch(`${API}/turmas?nivel=${nivel}&turno=${turno}`, { signal: controlador.signal })
      .then((r) => r.json())
      .then(setTurmas)
      .catch((e) => {
        if (e.name !== 'AbortError') throw e;
      });

    return () => controlador.abort();
  }, [nivel, turno]);

  return (
    <ul>
      {turmas.map((t) => (
        <li key={t.id}>{t.nome}</li>
      ))}
    </ul>
  );
}

Mesmo contador, dois segundos de tela aberta, e o turno trocando de manha para noite no meio do caminho:

5ms pedido 1: nivel=avancado turno=manha tela: <ul><li>Conversação Avançada — sáb 09h</li></ul> 1041ms pedido 2: nivel=avancado turno=noite tela: <ul><li>Conversação Avançada — qui 19h</li></ul> total: 2 pedidos

Dois pedidos, um por mudança de filtro. Prefira dependências primitivas — string, número e booleano. Se precisar de um objeto, desmonte seus campos ou estabilize a referência com useMemo, hook que memoriza um valor entre renderizações. O comportamento completo está na lição de useEffect.

Quando extrair a busca para um hook?

Extraia quando estados, tratamento e cancelamento começarem a se repetir. Um hook customizado agrupa essa lógica e entrega ao componente apenas o resultado.

Quatro estados, função assíncrona, cancelamento, resposta.ok e dependências somam cerca de trinta linhas. Repeti-las em várias telas aumenta o risco de cada cópia corrigir um erro e esquecer outro.

O primeiro passo é useTurmas. Seu nome começa com use porque ele chama outros hooks. O estado vira um objeto com um campo status — aqui, fase da interface, não status HTTP — evitando combinações como “carregando e com erro”:

jsx
function useTurmas(nivel, turno) {
  const [estado, setEstado] = useState({ status: 'carregando', turmas: [], erro: null });

  useEffect(() => {
    const controlador = new AbortController();
    setEstado({ status: 'carregando', turmas: [], erro: null });

    async function buscar() {
      try {
        const resposta = await fetch(`${API}/turmas?nivel=${nivel}&turno=${turno}`, {
          signal: controlador.signal,
        });
        if (!resposta.ok) throw new Error(`a secretaria respondeu ${resposta.status}`);
        const turmas = await resposta.json();
        setEstado({ status: turmas.length ? 'ok' : 'vazio', turmas, erro: null });
      } catch (e) {
        if (e.name === 'AbortError') return;
        setEstado({ status: 'erro', turmas: [], erro: e.message });
      }
    }

    buscar();
    return () => controlador.abort();
  }, [nivel, turno]);

  return estado;
}

O hook inicia em carregando, repete o efeito quando os filtros mudam e percorre as fases ok, vazio ou erro. A limpeza cancela a busca pendente, e return estado expõe o resultado sem expor os detalhes da rede.

O componente que usa esse hook cabe em dez linhas e não sabe nada sobre rede:

jsx
function PainelDeTurmas({ nivel, turno }) {
  const { status, turmas, erro } = useTurmas(nivel, turno);

  if (status === 'carregando') return <p>Carregando turmas…</p>;
  if (status === 'erro') return <p>Não deu para carregar as turmas: {erro}</p>;
  if (status === 'vazio') return <p>Nenhuma turma de {turno} neste nível.</p>;

  return (
    <ul>
      {turmas.map((t) => (
        <li key={t.id}>
          {t.nome}{t.vagas} vagas
        </li>
      ))}
    </ul>
  );
}

O componente desestrutura os três campos retornados e escolhe a interface pelo status. Só o caminho ok chega ao map da lista.

Montei esse painel três vezes com turno="noite": num nível que tem turma à noite, num nível sem turma nenhuma e num nível digitado errado.

avancado → <ul><li>Conversação Avançada — qui 19h — 3 vagas</li></ul> corporativo → <p>Nenhuma turma de noite neste nível.</p> intermediarrio → <p>Não deu para carregar as turmas: a secretaria respondeu 404</p>

O hook resolve repetição, e só. Ele não tem cache, uma cópia guardada de uma resposta para reutilização, nem revalida ao voltar para a aba ou tenta novamente após falha. Bibliotecas de dados oferecem esses recursos. Use esta régua:

situação abordagem por quê
uma ou duas telas com dado, projeto de estudo useEffect + fetch na mão você precisa entender o mecanismo antes de escondê-lo
a mesma busca em três telas hook próprio, como o useTurmas tira a repetição sem trazer dependência nova
lista que várias telas mostram, com cache e revalidação TanStack Query ou SWR cache por chave, tentativa nova e dado compartilhado saem prontos
o dado é da rota (/turmas/:id) loader do React Router busca começa junto com a navegação, não depois do render
o projeto é Next.js ou similar buscar no servidor o HTML já sai com a lista dentro; não existe estado de carregando

Numa tela, useEffect e fetch deixam o mecanismo visível. Quando outra tela repetir a busca, extraia um hook customizado. Quando várias telas precisarem compartilhar o dado carregado, considere uma biblioteca de dados: cache é um problema separado da interface.

O que aprender depois de consumir uma API?

Na ordem 13, aprenda useRef: ele guarda um valor entre renderizações sem pedir uma nova tela. Isso é útil para identificadores de temporizadores e outros recursos externos que também precisam de limpeza.

Rode a API local, monte os quatro estados e provoque a corrida: clique em “Básico” e logo depois em “Avançado”. Observe a tela antes e depois de ligar o AbortController na limpeza.

Até aqui: você modelou quatro estados, tratou rede e status HTTP, cancelou buscas antigas, corrigiu dependências e extraiu um hook sem confundi-lo com cache.

O próximo passo da trilha de React é useRef no React, onde você guardará, entre outros valores, o identificador de um setTimeout que precisa ser cancelado.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • react
  • api
  • fetch
  • useeffect
  • async

Perguntas frequentes

Preciso do axios ou dá para usar só o fetch?
Dá para usar só o fetch: ele é nativo no navegador e no Node desde a versão 18. O axios entrega alguns atalhos (JSON automático, erro em status 4xx, interceptadores), e é por isso que times grandes gostam dele. Para aprender, comece pelo fetch — o que você entender ali vale para qualquer cliente HTTP.
Onde eu guardo o endereço da API?
Numa variável de ambiente, nunca espalhada pelos componentes. Em projeto Vite, uma linha VITE_API_URL=http://localhost:4517 no arquivo .env e import.meta.env.VITE_API_URL no código. Lembre que tudo que vai para o front é público: chave secreta fica no back-end.
Posso chamar a API direto no onClick, sem useEffect?
Pode, e em muitos casos é o certo. Efeito serve para sincronizar a tela com algo de fora quando ela aparece ou quando um filtro muda. Ação disparada por gente — salvar matrícula, excluir turma — mora no manipulador do evento, não no efeito.
O React 19 não tem o use() para isso?
Tem, e ele lê uma promise direto no render, junto com Suspense. Só que alguém precisa criar essa promise fora do componente e guardá-la em cache, senão cada render dispara uma nova. Quem faz esse trabalho é o framework ou a biblioteca de dados — não é substituto direto do useEffect num projeto Vite puro.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0 com React 19.2.8 e jsdom 30.0.1, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — AbortController — developer.mozilla.org
  2. MDN — Usando a Fetch API — developer.mozilla.org
  3. React — useEffect: buscando dados — react.dev

Continue por aqui