React: Solicitações HTTP e recuperação de dados
Última atualização: 2026-08-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
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
import { useState, useEffect } from 'react'
function UserList() {
const [users, setUsers] = useState([]) // Data
const [loading, setLoading] = useState(true) // Loaded state
const [error, setError] = useState(null) // Error State
function fetchUsers() {
setLoading(true)
setError(null)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}:${response.statusText}`)
}
return response.json()
})
.then(data => {
setUsers(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}
useEffect(() => {
fetchUsers()
}, [])
// --- Three-State Rendering ---
if (loading) {
return (
<div className="loading-state">
<div className="spinner" />
<p>Loading user data...</p>
</div>
)
}
if (error) {
return (
<div className="error-state">
<p className="error-icon">⚠</p>
<p>Failed to load:{error}</p>
<button onClick={fetchUsers}>Retry</button>
</div>
)
}
if (users.length === 0) {
return (
<div className="empty-state">
<p>No user data available</p>
</div>
)
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
)
}
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
import { useState, useEffect } from 'react'
// General Data Request Hook
function useFetch(fetchFn, deps = []) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
function execute() {
setLoading(true)
setError(null)
fetchFn()
.then(result => {
setData(result)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}
useEffect(() => {
execute()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
return { data, loading, error, refetch: execute }
}
// ====== Usage ======
function UserList() {
const { data: users, loading, error, refetch } = useFetch(
() => fetch('https://jsonplaceholder.typicode.com/users')
.then(r => { if (!r.ok) throw new Error('Request Failed'); return r.json() }),
[]
)
if (loading) return <p>Loading......</p>
if (error) return <p>Error:{error} <button onClick={refetch}>Retry</button></p>
return (
<ul>
{users?.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
)
}
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
▶ 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
}
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>
)
}
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>
)
}
▶ 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)
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
P: O que devo escolher, fetch ou axios? R: O fetch é uma API integrada ao navegador que não requer dependências e é adequada para solicitações simples. No entanto, ela exige o tratamento manual dos códigos de status de erro HTTP (erros 4xx/5xx não acionam o bloco
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: usefetchpara projetos pequenos eaxiospara projetos grandes.
P: Por que a gestão de três estados usa três ganchos
useStateseparados em vez de um único objeto? R: Três ganchosuseStateseparados 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.
P: O servidor ainda processa a solicitação depois que o AbortController a interrompe? R: Sim. O AbortController apenas impede que o front-end aguarde a resposta; o servidor ainda recebe e processa a solicitação (ele não pode impedir que a solicitação chegue ao servidor). Para operações de gravação (POST/PUT/DELETE), o back-end deve ser projetado para ser idempotente, ou o front-end deve implementar medidas para evitar envios duplicados.
P: Se o hook personalizado
useFetchfor chamado em vários componentes, eles compartilharão o estado? R: Não. Cada vez que um componente chamauseFetch(), ele cria um escopo independente (closure), de modo que os respectivos valores dedata,loadingeerrornã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).
P: O que devo levar em consideração ao lidar de maneira uniforme com redirecionamentos 401 para a página de login em um interceptador de resposta? R: Tome cuidado para evitar um loop infinito de redirecionamentos — se a própria página de login fizer uma solicitação de API (para verificar a validade do token) e essa solicitação também retornar um 401, isso resultará em um loop infinito: “solicitação → 401 → redirecionamento → solicitação → 401”. Solução: Verifique o caminho da página atual no interceptador; se o usuário já estiver em
/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.