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



2. Diagramas conceituais

100%
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:

JSX
const [data, setData] = useState(null)     // Success Data
const [loading, setLoading] = useState(true) // Loading...
const [error, setError] = useState(null)    // Error Message
▶ Experimente

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

JSX 📖 Somente leitura
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">&#x26A0;</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>
  )
}
61 linhas de lógica (limite de 40, somente leitura)

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

JSX
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>
  )
}
▶ Experimente

Vantagens dos ganchos:

  1. O código do componente foi significativamente simplificado para se concentrar na lógica de renderização.
  2. Manutenção unificada da lógica de três estados; as alterações precisam ser feitas apenas em um único local
  3. A função refetch está 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.

BASH
npm install axios

▶ Exemplo 3: Envolvendo uma instância do axios

JSX 📖 Somente leitura
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
}
84 linhas de lógica (limite de 40, somente leitura)

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

JSX 📖 Somente leitura
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>
  )
}
45 linhas de lógica (limite de 40, somente leitura)

Principais mecanismos:

  1. Sempre que query muda, a função de limpeza do useEffect chama controller.abort() para cancelar a solicitação anterior.
  2. 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.
  3. 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

JSX 📖 Somente leitura
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>
  )
}
47 linhas de lógica (limite de 40, somente leitura)

▶ 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:

JSX
// 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)
▶ Experimente

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: use fetch para projetos pequenos e axios para projetos grandes.

P: Por que a gestão de três estados usa três ganchos useState separados em vez de um único objeto? R: Três ganchos 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.

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 useFetch for chamado em vários componentes, eles compartilharão o estado? R: Não. Cada vez que um componente chama 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).

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


📝 Exercícios

  1. Crie um componente de lista de usuários: use uma solicitação de busca para recuperar dados de https://jsonplaceholder.typicode.com/users e 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.
  2. Com base na tarefa acima, extraia a lógica de três estados para um hook personalizado useFetch e, em seguida, utilize-o em dois componentes diferentes para verificar se os estados são independentes.
  3. 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%