Para buscar dados no React, inicie a requisição dentro de useEffect, guarde a resposta no estado e renderize uma mensagem de carregamento até ela chegar. Coloque os valores de que a requisição depende, como um id, no array de dependências para que o efeito busque de novo quando eles mudarem.
Os exemplos desta página não conseguem acessar a rede, então fetchUser é uma API falsa: uma promise que resolve depois de um atraso, como o fetch faz. Troque 800 por 3000 para ver o texto de carregamento ficar mais tempo.
Com uma API real, o corpo do efeito fica assim:
useEffect(() => {
fetch('https://api.example.com/users/1')
.then((res) => res.json())
.then((data) => setUser(data));
}, []);
Loading, erro e dados
Uma requisição tem três resultados que a tela deve mostrar: ainda carregando, falhou ou terminou. Guarde cada um no estado e reinicie-os quando o efeito começar uma nova requisição.
Clique em User 3: a API falsa rejeita, a mensagem de erro substitui o nome e o console mostra o erro. O bloco finally desliga loading nos dois caminhos, então uma requisição que falhou nunca deixa o spinner ligado.
Quando o fetch conversa com um servidor real, uma resposta 404 ou 500 não rejeita a promise. Confira res.ok e lance o erro você mesmo:
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
async/await dentro de um efeito
A função que você passa para useEffect não pode ser async. O React espera que ela retorne nada ou uma função de limpeza, e uma função async sempre retorna uma promise.
// Wrong: the effect returns a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// Right: define an async function inside and call it
useEffect(() => {
async function load() {
const data = await fetchUser(id);
setUser(data);
}
load();
}, [id]);
Condições de corrida
Quando o id muda rápido, duas requisições ficam em andamento ao mesmo tempo. Nada garante que elas respondam em ordem. Se a mais antiga for mais lenta, ela chega por último e sobrescreve os dados do id que o usuário escolheu mais recentemente.
A solução é um indicador ignore. Cada execução do efeito tem o próprio indicador, e a limpeza dela o define como true. Como o React executa a limpeza antes da próxima execução, uma resposta de uma execução desatualizada vê ignore === true e é descartada. Este bloco renderiza o mesmo perfil duas vezes, sem e com o indicador:
Clique no botão e espere. A resposta rápida do usuário 2 chega primeiro, então os dois perfis registram e ficam com Grace. Cerca de um segundo e meio depois do clique, chega a resposta lenta do usuário 1: o perfil sem o indicador muda para Ada mesmo com o usuário 2 selecionado, enquanto o que tem o indicador registra dropped stale Ada e continua com Grace.
O mesmo bug aparece em caixas de busca, abas e qualquer lista filtrada pelo servidor. O exemplo de loading acima deixa o indicador de fora para ficar curto, mas toda busca em um efeito deveria tê-lo, ou o abort abaixo.
Cancelando com AbortController
O indicador ignore descarta uma resposta desatualizada, mas a requisição continua até o fim. Com o fetch real você pode cancelá-la. Crie um AbortController no efeito, passe o signal dele para o fetch e chame abort() na limpeza:
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setUser(await res.json());
} catch (err) {
if (err.name === 'AbortError') return; // cancelled on purpose
setError(err);
}
}
load();
return () => controller.abort();
}, [id]);
Um fetch abortado rejeita com um AbortError, então o bloco catch o pula em vez de mostrá-lo como falha. O abort também cobre a desmontagem: quando o componente é removido, a limpeza dele cancela a requisição.
Levando para um hook personalizado
Loading, erro e a proteção contra corridas são iguais em todo componente que busca dados. Coloque-os em um hook personalizado e cada componente pede dados em uma linha.
fetchPosts é definida fora do componente, então a identidade dela nunca muda e ela é segura no array de dependências. Definida dentro de App, ela seria uma função nova a cada renderização, o efeito executaria depois de toda renderização e, como o efeito define estado, ele nunca pararia de buscar.
Troque 500 por 2000 e clique no botão duas vezes seguidas: o console mostra duas requisições, e só aparecem os posts do tema em que você parou.
Buscando em um event handler
Um efeito serve para dados de que o componente precisa porque está na tela: uma página de perfil carrega o perfil. Quando uma requisição acontece porque o usuário fez algo, como apertar Search ou Save, faça-a no event handler. Você sabe exatamente o que a disparou, e nada executa de novo quando um estado sem relação muda.
async function handleSubmit(e) {
e.preventDefault();
setStatus('saving');
const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
setStatus(res.ok ? 'saved' : 'error');
}
Erros comuns
Dependências faltando. Um efeito que lê id mas tem [] busca o primeiro usuário para sempre. Liste todos os valores que a requisição usa.
Buscar no corpo do componente. Um fetch fora de um efeito executa a cada renderização e, se define estado, inicia um loop.
Confiar que o fetch rejeita em erros. Ele só rejeita quando a rede falha. Confira res.ok.
Esquecer de reiniciar o loading. Quando o id muda, volte loading para true, ou os dados antigos ficam na tela sem nenhum sinal de que dados novos estão a caminho.
Quando usar uma biblioteca ou um framework
Buscar em um efeito funciona bem para poucas requisições. Não há cache: abra o mesmo perfil duas vezes e ele busca duas vezes. Também não compartilha dados entre componentes, não tenta de novo quando falha nem recarrega quando a aba volta a ter foco. As bibliotecas fazem isso por você:
import { useQuery } from '@tanstack/react-query';
function Profile({ id }) {
const { data, error, isPending } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return <p>{data.name}</p>;
}
TanStack Query e SWR são as escolhas comuns para dados no cliente. Os frameworks vão além e carregam os dados no servidor antes de a página chegar ao navegador: o Next.js com server components, o React Router com loaders. Isso evita o flash de carregamento e a cadeia de requisições que começa quando os dados de um pai precisam chegar antes que um filho possa sequer começar a buscar.
React 19: use() com Suspense
O React 19 adiciona o use, que lê uma promise durante a renderização. O componente fica suspenso até a promise resolver, e o <Suspense> mais próximo mostra um fallback enquanto isso, então o próprio componente não tem estado de loading:
import { use, Suspense } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>
A promise precisa ser criada fora do componente (por um framework, um cache ou um pai), não dentro da renderização que a lê, ou cada renderização inicia uma nova requisição. A página do hook use trata disso com exemplos executáveis.
Perguntas frequentes
Como busco dados quando um componente React carrega?
Inicie a requisição em um useEffect com os valores de que ela depende no array de dependências ([] se não houver nenhum) e guarde o resultado no estado com useState. Renderize uma mensagem de carregamento até os dados chegarem.
Por que o callback do useEffect não pode ser async?
Uma função async sempre retorna uma promise, e o React espera que o efeito retorne nada ou uma função de limpeza. Escreva uma função async dentro do efeito e chame-a logo em seguida.
O que é uma condição de corrida na busca de dados no React?
Quando a entrada muda rápido, duas requisições ficam em andamento e a mais antiga pode responder por último, sobrescrevendo os dados mais novos. Defina um indicador ignore na limpeza do efeito e pule o setState quando ele estiver ligado, ou cancele a requisição com um AbortController.
Devo usar useEffect ou uma biblioteca para buscar dados?
O useEffect funciona para apps pequenos e vale a pena entender. Para cache, eliminação de duplicadas, novas tentativas e recarregamento, uma biblioteca como TanStack Query ou SWR, ou o carregamento de dados embutido em um framework como o Next.js, poupa você de escrever essa lógica.
Como mostro um spinner enquanto os dados carregam?
Mantenha um booleano loading (ou uma string status) no estado, ligue-o antes de a requisição começar e desligue-o quando ela terminar, tanto no caminho de sucesso quanto no de erro. Renderize o spinner enquanto ele for verdadeiro.