Buscando dados
Um componente que mostra dados de um servidor não possui esses dados no momento em que renderiza pela primeira vez. Precisa pedir, esperar e então mostrar algo quando a resposta chega, ou se a requisição falhar pelo caminho. Este capítulo cobre o padrão que aplicações React usam para isso: fazer fetch dentro de um effect e manter o resultado, um estado de carregamento e um erro em useState.
Aqui está um componente Profile que busca um usuário por URL. A renderização acontece primeiro, e chamar o servidor logo depois é exatamente o momento para o qual useEffect existe:
function Profile({ url }) {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let active = true
setData(null)
setError(null)
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`Request failed: ${r.status}`)
return r.json()
})
.then(d => { if (active) setData(d) })
.catch(e => { if (active) setError(e) })
return () => { active = false }
}, [url])
if (error) return <p>Failed to load</p>
if (!data) return <p>Loading...</p>
return <h1>{data.name}</h1>
}Se fetch, promises ou .then ainda são desconhecidos, o manual de JavaScript cobre tudo do zero em Async JavaScript; tudo aqui se baseia direto nisso.
useEffect é o hook que executa código depois que React renderiza um componente, em vez de durante a renderização em si. Você passa uma função para executar e um array de dependências, a lista de valores que devem fazer ele rodar de novo quando mudam, aqui só url. Profile começa com duas peças de estado: data para o resultado e error para qualquer coisa que deu errado. No topo do effect, setData(null) e setError(null) limpam o que a execução anterior deixou para trás. Isso importa por duas razões: sem isso, uma mudança de url continuaria mostrando o perfil antigo em vez de "Loading...", já que data ainda contém o último resultado bem-sucedido, e uma requisição que sucede depois de uma anterior ter falhado nunca limparia o erro antigo, já que nada volta a colocar error em null. Depois fetch retorna uma promise para a resposta. fetch só rejeita essa promise em uma falha de rede, então um 404 ou 500 ainda resolvem normalmente como um objeto response, por isso o primeiro .then checa r.ok e lança antes da resposta chegar em .json(). Pule essa checagem e uma requisição falhada fluiria direto para data como se tivesse sucedido. Uma vez que a checagem passa, .json() lê o body e o resultado cai em data através de setData. Se qualquer coisa disso rejeita, .catch coloca o problema em error no lugar. Lá embaixo no JSX, error e data são checados como qualquer outro estado: um erro renderiza uma mensagem, nenhum dado ainda renderiza uma mensagem de carregamento, e dados renderizam a UI real.
Não há uma variável de carregamento separada aqui. Enquanto data e error forem ambos ainda null, o componente está entre a requisição sair e algo voltar, então if (!data) return <p>Loading...</p> cobre essa lacuna sozinho. Alguns componentes adicionam um booleano isLoading dedicado para controle mais fino, mas derivar carregamento do estado que você já tem é muitas vezes mais simples, e é um valor a menos para manter sincronizado.
A variável active dentro do effect é uma flag de limpeza. useEffect pode retornar uma função, e React chama essa função logo antes do effect rodar de novo e quando o componente é desmontado. Aqui, a função retornada coloca active em false, e tanto .then quanto .catch checam ela antes de chamar setData ou setError. Sem essa checagem, uma requisição ainda em voo quando url muda, ou quando o componente sai da tela completamente, eventualmente resolveria e tentaria atualizar estado que não pertence mais ao que está sendo mostrado. A flag transforma uma resposta tardia em uma no-op em vez de um bug.
Fazer fetch à mão assim vale a pena entender, porque é o padrão no qual quase tudo mais se baseia. Em uma aplicação real você normalmente vai para uma biblioteca de data-fetching como React Query, ou o carregamento de dados integrado de um framework (Next.js e frameworks similares lidam com isso na camada de roteamento), em vez de escrever useEffect e alguns pedaços de estado para cada requisição. Essas ferramentas lidam com cache, retries e os problemas de timing abaixo para você. O padrão neste capítulo é o que elas são construídas sobre.
useEffect, mantenha data e error em estado, e checá-los no seu JSX para decidir o que mostrar. Carregamento é o momento em que ambos ainda estão vazios. A flag active para uma resposta tardia de colocar estado em um componente que não está mais mostrando esses dados.
Próximo: Refs e o DOM, a válvula de escape para os trabalhos que precisam do nó DOM em si.

