Menu

Buscar dados no React: useEffect, loading, erros, corridas

Busque dados no React iniciando a requisição no useEffect, guardando loading, erro e dados no estado e ignorando respostas que chegam depois que as entradas mudaram. Trata de async/await em efeitos, AbortController, condições de corrida e quando usar uma biblioteca.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR