React: Solicitações HTTP e recuperação de dados
Última atualização: 2026-10-26
Quando Tom chamou a API para recuperar uma lista de usuários na página de gerenciamento de usuários, surgiram três problemas: primeiro, a alternância rápida entre páginas fazia com que a resposta da solicitação anterior sobrescrevesse os dados da solicitação seguinte (uma condição de corrida); em segundo lugar, quando ocorria um erro de rede, a página simplesmente ficava em branco, sem exibir nenhuma mensagem de erro; e, finalmente, todas as páginas precisavam implementar repetidamente a mesma lógica de três estados — carregando, erro e dados. Ele percebeu que era necessária uma solução unificada para solicitações HTTP a fim de gerenciar todo o ciclo de vida das solicitações.
1. O que você vai aprender
- Critérios para escolher entre a Fetch API e o Axios
- Melhores práticas para gerenciar os estados “Carregando”, “Erro” e “Dados”
- AbortController: Cancela solicitações para evitar condições de corrida
- Encapsulamento de instâncias do Axios e configuração de interceptadores
- Tratamento de erros e estratégias de repetição automática
2. Diagramas conceituais
flowchart LR
A[Submit a Request] --> B{loading = true}
B --> C[Request in progress]
C --> D{Success/Failure?}
D -->|Success| E[data = Response<br/>loading = false<br/>error = null]
D -->|Failure| F[error = Error Message<br/>loading = false<br/>data = null]
E --> G[Rendering Data]
F --> H{Can I try again??}
H -->|is | A
H -->|No| I[Display Error UI]
G --> J[Component Uninstallation?]
J -->|is | K[AbortController<br/>Cancel Request]
style B fill:#fff3e0,stroke:#f57c00
style D fill:#e1f5fe,stroke:#0288d1
style K fill:#ffcdd2,stroke:#d32f2f
Ciclo de vida da solicitação: o carregamento é iniciado → a solicitação é executada → os dados são atribuídos em caso de sucesso / um erro é atribuído em caso de falha → as solicitações pendentes são canceladas quando o componente é desmontado.
3. Um cenário da vida real
A página de gerenciamento de usuários do Tom exige o seguinte: a lista de usuários deve ser carregada assim que a página for aberta; um indicador de carregamento deve ser exibido enquanto a lista está sendo carregada; uma mensagem de erro e um botão “Tentar novamente” devem aparecer caso o carregamento falhe; os dados não devem ficar desorganizados quando os usuários alternarem rapidamente entre as visualizações de lista e de detalhes; e todas as solicitações de API devem redirecionar automaticamente para a página de login caso ocorra um erro 401.
(1) Modo de três estados
A abordagem mais básica para fazer solicitações HTTP no React consiste em gerenciar três variáveis de estado:
const [data, setData] = useState(null) // Success Data
const [loading, setLoading] = useState(true) // Loading...
const [error, setError] = useState(null) // Error Message
Saída:
Busca assíncrona de dados com estados de carregamento/erro/sucesso
Por que é necessária uma separação em três estados? Porque a interface do usuário precisa exibir conteúdos completamente diferentes em três situações distintas:
| Status | Dados | Carregando | Erro | Comportamento da interface do usuário |
|---|---|---|---|---|
| Carregando | null | true | null | Mostrar indicador de carregamento ou tela de espera |
| Sucesso | Dados | falso | nulo | Exibir lista de dados |
| Falha | nulo | falso | Mensagem de erro | Exibir mensagem de erro e botão para tentar novamente |
| Dados vazios | [] | false | null | Exibir a mensagem “Não há dados disponíveis” |
▶ Exemplo 1: Gerenciamento de três estados com fetch e useEffect
▶ Expandir exemplo executável (61 linhas)
Saída:
Busca de dados: ⏳ "Loading..." → ✅ dados exibidos → ou ❌ "Error: Network request failed" com botão de tentar novamente
Saída:
Exibe: "Loading user data...". Estado: users (setter: setUsers), loading (setter: setLoading), error (setter: setError). Botão: Retry. useEffect gerencia efeitos colaterais. Busca assíncrona de dados / estados de carregamento
Observação importante: fetch() só lança uma exceção quando ocorre um erro de rede; os códigos de status HTTP 4xx/5xx não acionam o bloco catch. Portanto, você deve verificar manualmente response.ok (ou response.status) dentro de then e lançar proativamente um erro para respostas que não sejam 2xx.
(2) Extração de ganchos personalizados
É claramente impraticável escrever a lógica de três estados repetidamente em cada página. Tom extraiu a lógica de três estados para um hook personalizado, que pode ser usado em qualquer componente com apenas uma linha de código.
▶ Exemplo 2: O gancho personalizado useFetch
▶ Experimente
Saída:
O hook customizado retorna { data, loading, error }. Uso: const { data, loading } = useFetch("/api/users"). Busca automaticamente na montagem.
Vantagens dos ganchos:
- O código do componente foi significativamente simplificado para se concentrar na lógica de renderização.
- Manutenção unificada da lógica de três estados; as alterações precisam ser feitas apenas em um único local
- A função
refetchestá disponível no componente para facilitar o acionamento manual de uma nova solicitação
4. Wrappers avançados do axios
| Recurso | fetch | axios |
|---|---|---|
| Instalação | Integrado ao navegador | npm install axios |
| Análise da resposta | Manual res.json() |
Conversão automática para JSON |
| Interceptação de solicitação/resposta | Sem recurso integrado | Interceptador interceptors |
| Configurações de tempo limite | Deve ser usado com as opções AbortController |
timeout |
| Tratamento de erros | Erros HTTP 4xx/5xx não lançam exceções | Erros HTTP são lançados automaticamente |
| Cancelamento de solicitação | AbortController |
CancelToken (antigo) / AbortController (novo) |
| TypeScript | Necessária asserção manual de tipo | Genéricos axios.get<T>() |
À medida que o projeto crescia, Tom percebeu que precisava adicionar tokens manualmente, lidar com redirecionamentos 401 e definir tempos de espera para cada solicitação — o que era extremamente tedioso. Os mecanismos de instanciação e interceptadores do Axios podem resolver todos esses problemas de uma só vez.
npm install axios
Saída:
Exibe: "Loading......". Estado: data (setter: setData), loading (setter: setLoading), error (setter: setError). Botão: Retry. Lista: {u.name}. useEffect gerencia efeitos colaterais. Busca assíncrona de dados / estados de carregamento
▶ Exemplo 3: Envolvendo uma instância do axios
import axios from 'axios'
// Get token(from localStorage or auth store)
function getToken() {
return localStorage.getItem('auth_token')
}
// Create axios Examples
const api = axios.create({
baseURL: '/api/v1', // Basic Path
timeout: 10000, // Timeout (10s)
headers: {
'Content-Type': 'application/json',
}
})
// ========== Request Interceptor ==========
api.interceptors.request.use(
config => {
// Auto-add Authorization header
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// Request Logs(Development Environment)
if (process.env.NODE_ENV === 'development') {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`, config.params || '')
}
return config
},
error => {
console.error('[API] Request configuration error:', error)
return Promise.reject(error)
}
)
// ========== Response Interceptors ==========
api.interceptors.response.use(
// Successful Response:Return directly data Field(Remove the outer packaging)
response => response.data,
// Failure Response:Unified Error Handling
error => {
if (error.response) {
// The server returned an error status code
const { status, data } = error.response
switch (status) {
case 401:
// Unauthorized → Clear token,Go to the Login Page
localStorage.removeItem('auth_token')
window.location.href = '/login'
break
case 403:
console.warn('[API] Access Denied')
break
case 404:
console.warn('[API] Resource does not exist')
break
case 500:
console.error('[API] Internal Server Error')
break
default:
console.error(`[API] HTTP ${status}:`, data?.message || 'Unknown error')
}
return Promise.reject(new Error(data?.message || `HTTP ${status}`))
}
if (error.code === 'ECONNABORTED') {
// Request timed out
return Promise.reject(new Error('Request timed out,Please check your internet connection.'))
}
// Network error (offline, DNS failure, etc.)
return Promise.reject(new Error('Network Connection Error'))
}
)
// ========== Export the packaged API Methods ==========
export const userApi = {
getList: (params) => api.get('/users', { params }),
getById: (id) => api.get(`/users/${id}`),
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`),
}
export const productApi = {
getList: (params) => api.get('/products', { params }),
getById: (id) => api.get(`/products/${id}`),
}
// ========== Used in components ==========
function UserTable() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
userApi.getList({ page: 1, limit: 20 })
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
// ... Rendering Logic
}
Saída:
Estado: query, results, loading. Usa useeffect
Saída:
... (`[API] ${config.method?.toUpperCase()
O poder dos interceptadores: Os interceptadores de solicitação inserem tokens automaticamente, e os interceptadores de resposta lidam automaticamente com redirecionamentos 401 — os componentes e os chamadores de API não precisam se preocupar nem um pouco com essas questões transversais.
5. Cancelamento de inscrições e condições da corrida
| Cenários competitivos | Causas | Soluções |
|---|---|---|
| Condição de corrida na entrada de pesquisa | Entradas rápidas acionam múltiplas solicitações; respostas antigas sobrescrevem as novas | O AbortController cancela as solicitações antigas |
| Condição de corrida na troca de página | Respostas a solicitações antigas ainda chegam após a troca de página | Cancelamento da limpeza do useEffect |
| Cliques repetidos no botão | O usuário clica no botão “Enviar” várias vezes | Desativar os botões “Enviar” e “Cancelar” durante a solicitação |
| Condição de corrida na alternância de abas | A alternância rápida entre abas causa desalinhamento de dados | Use o sinalizador ignore para ignorar respostas antigas |
Essa é a armadilha mais sutil que Tom já encontrou. Quando um usuário faz uma pesquisa rápida em um campo de entrada — digitando “a” → “ab” → “abc” → “abcd” —, se a velocidade da rede variar, pode ocorrer o seguinte: a resposta para “abcd” chega primeiro, seguida pela resposta para “a” (porque a solicitação anterior não foi cancelada). Como resultado, a página exibe o resultado para “a” em vez do resultado mais recente, “abcd”.
Esse fenômeno é chamado de condição de corrida. Solução: antes de iniciar uma nova solicitação, cancele a solicitação anterior que ainda não foi concluída.
▶ Exemplo 4: AbortController — Cancelamento de uma solicitação
import { useState, useEffect } from 'react'
function SearchUsers() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) {
setResults([])
return
}
// Create AbortController
const controller = new AbortController()
const signal = controller.signal
setLoading(true)
fetch(`/api/users/search?q=${encodeURIComponent(query)}`, { signal })
.then(res => res.json())
.then(data => {
setResults(data)
setLoading(false)
})
.catch(err => {
// Handle only non-canceled errors
if (err.name !== 'AbortError') {
console.error('Search Failed:', err)
setLoading(false)
}
})
// Cleanup Function:Component uninstallation or query Cancel the request when changes occur
return () => {
controller.abort()
}
}, [query])
return (
<div>
<input
placeholder="Search Users..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
{loading && <p>Searching......</p>}
<ul>
{results.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
Saída:
Busca de dados: ⏳ "Loading..." → ✅ dados exibidos → ou ❌ "Error: Network request failed" com botão de tentar novamente
Saída:
Exibe: "Searching......". Estado: query (setter: setQuery), results (setter: setResults), loading (setter: setLoading). Entrada: Search Users.... Lista: {user.name}. useEffect gerencia efeitos colaterais. Busca assíncrona de dados / estados de carregamento
Principais mecanismos:
- Sempre que
querymuda, a função de limpeza do useEffect chamacontroller.abort()para cancelar a solicitação anterior. - As solicitações canceladas entram no ramo “catch” e são filtradas pelo
err.name !== 'AbortError'— isso evita que a interface de erro seja acionada por engano. - Em última análise, apenas a resposta à última solicitação acionará
setResults, eliminando completamente a condição de corrida.
▶ Exemplo 5: Cancelamento de uma solicitação do axios
import { useState, useEffect } from 'react'
import axios from 'axios'
function SearchProducts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (!query.trim()) {
setResults([])
return
}
// axios Cancel Token
const source = axios.CancelToken.source()
setLoading(true)
axios.get('/api/products/search', {
params: { q: query },
cancelToken: source.token
})
.then(res => {
setResults(res.data)
setLoading(false)
})
.catch(err => {
if (!axios.isCancel(err)) {
console.error('Search Failed:', err)
setLoading(false)
}
// Cancelled requests are not processed
})
return () => {
source.cancel('The request has been canceled') // Reason for Cancellation
}
}, [query])
return (
<div>
<input
placeholder="Search for Products..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
{loading && <p>Searching......</p>}
<ul>
{results.map(p => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
</div>
)
}
Saída:
Axios: axios.get("/api/users") → response.data. Parse automático de JSON, interceptors e timeout. Mais simples que o fetch.
Saída:
Exibe: "Searching......". Estado: query (setter: setQuery), results (setter: setResults), loading (setter: setLoading). Entrada: Search for Products.... Lista: {p.name} — ${p.price}. useEffect gerencia efeitos colaterais
▶ Exemplo 6: Estratégia de repetição automática
Como as solicitações de rede não são confiáveis, Tom quer que elas sejam repetidas automaticamente quando falharem (por exemplo, tentar duas vezes com intervalos que aumentam gradualmente). O Axios não possui um recurso de repetição integrado, mas isso pode ser facilmente implementado usando um interceptador:
// Retry Interceptor
function setupRetryInterceptor(axiosInstance, maxRetries = 2) {
axiosInstance.interceptors.response.use(
response => response,
async error => {
const config = error.config
// Cases Where Retry Is Not Performed:Not configured、I've already tried again、It's not a network error
if (!config || config._retryCount >= maxRetries) {
return Promise.reject(error)
}
// Only in the event of a network error or 5xx Retry in Case of a Server Error
const status = error.response?.status
if (status && status < 500) {
return Promise.reject(error)
}
config._retryCount = (config._retryCount || 0) + 1
// Exponential backoff: 1st retry after 1s, 2nd retry after 2s
const delay = config._retryCount * 1000
await new Promise(r => setTimeout(r, delay))
console.log(`[API] Retry ${config._retryCount}/${maxRetries}: ${config.url}`)
return axiosInstance(config)
}
)
}
// When using
setupRetryInterceptor(api, 2)
Saída:
Interceptor de nova tentativa: uma requisição que falha é repetida automaticamente com backoff exponencial (1s → 2s → 4s). O console registra cada tentativa ("Attempt 1 failed, retrying in 1000ms" …) e a resposta bem-sucedida é devolvida a quem chamou. O número máximo de tentativas é 3 — depois disso o erro é propagado sem alterações
O backoff exponencial é uma estratégia padrão de repetição de tentativas: o tempo de espera aumenta a cada nova tentativa, para evitar continuar sobrecarregando o servidor quando ele já está sobrecarregado.
❓ Perguntas Frequentes
catch) e não oferece suporte ao monitoramento do andamento da solicitação. O axios analisa JSON automaticamente, oferece suporte a interceptadores de solicitação/resposta, facilita o cancelamento de solicitações e oferece suporte ao andamento do upload. Recomendação: use fetch para projetos pequenos e axios para projetos grandes.useState separados em vez de um único objeto?useState separados permitem que os componentes se inscrevam com precisão para receber notificações sobre alterações em uma parte específica do estado. Se você usar um único objeto { data, loading, error }, qualquer alteração em qualquer campo fará com que os componentes inscritos nesse objeto sejam renderizados novamente. No entanto, no desenvolvimento prático, não há muita diferença entre as duas abordagens; portanto, basta escolher aquela com a qual você se sentir mais à vontade.useFetch for chamado em vários componentes, eles compartilharão o estado?useFetch(), ele cria um escopo independente (closure), de modo que os respectivos valores de data, loading e error não interferem uns nos outros. Se você precisar compartilhar dados de solicitação entre componentes (como quando dois componentes exibem uma lista de usuários), será necessário usar o gerenciamento de estado global + TanStack Query (veja a próxima lição)./login, não o redirecione.📖 Resumo
- O gerenciamento de três estados (carregando / erro / dados) é a base das solicitações de dados no React; esses quatro estados da interface do usuário abrangem todos os cenários.
- Use hooks personalizados para extrair a lógica de três estados, evitando a duplicação de código em cada componente e permitindo uma manutenção centralizada
- Instância do Axios + interceptadores para implementar a injeção automática de tokens, redirecionamentos 401 automáticos e tratamento unificado de erros
- O AbortController (fetch) / CancelToken (axios) cancela as solicitações pendentes, resolvendo completamente as condições de corrida
- O cancelamento da solicitação é necessário em situações de alterações frequentes, como pesquisas, navegação entre páginas e alternância entre abas.
📝 Exercícios
- Crie um componente de lista de usuários: use uma solicitação de busca para recuperar dados de
https://jsonplaceholder.typicode.com/userse implemente quatro estados da interface do usuário: carregando (ícone giratório), erro (mensagem de erro + botão de nova tentativa), sem dados (nenhum dado disponível) e exibição normal. - Com base na tarefa acima, extraia a lógica de três estados para um hook personalizado
useFetche, em seguida, utilize-o em dois componentes diferentes para verificar se os estados são independentes. - Crie um componente de pesquisa: faça com que o campo de entrada realize a pesquisa com base na entrada do usuário (simulando um atraso de 500 ms na API), use o AbortController para cancelar a solicitação anterior não concluída e verifique se os dados não ficam distorcidos ao digitar rapidamente.