Requisições HTTP no React com fetch e axios

1. Introdução às requisições HTTP no ecossistema React

No desenvolvimento front-end moderno, as APIs são a espinha dorsal da comunicação entre o cliente e o servidor. No React, consumir dados de APIs externas é uma tarefa cotidiana — seja para carregar uma lista de usuários, enviar um formulário ou autenticar um usuário.

Duas abordagens dominam esse cenário: a Fetch API nativa do JavaScript e a biblioteca Axios. Ambas permitem realizar requisições HTTP, mas diferem em sintaxe, recursos e nível de abstração. Compreender ambas é essencial para qualquer desenvolvedor React, pois cada uma tem seu lugar — fetch é leve e nativa, enquanto axios oferece uma experiência mais robusta e produtiva.

Toda requisição HTTP segue um ciclo de vida básico: loading (enquanto a requisição está sendo processada), sucesso (dados retornados) ou erro (falha na comunicação ou resposta inesperada). Gerenciar esses três estados é fundamental para criar interfaces responsivas e amigáveis.

2. Usando a Fetch API nativa no React

A Fetch API é nativa do JavaScript moderno e não requer instalação. Sua sintaxe básica envolve chamar fetch() com a URL e, em seguida, processar a resposta com .json().

// GET simples com fetch
fetch('https://api.exemplo.com/usuarios')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Erro:', error));

Para operações CRUD completas:

// POST com fetch
const novoUsuario = { nome: 'João', email: 'joao@email.com' };

fetch('https://api.exemplo.com/usuarios', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(novoUsuario)
})
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(data => console.log('Criado:', data))
  .catch(error => console.error('Erro:', error));

// PUT e DELETE seguem o mesmo padrão, alterando method e body

O tratamento de erros no fetch exige cuidado: a promessa do fetch() só rejeita em casos de falha de rede. Para erros HTTP (404, 500), precisamos verificar response.ok manualmente:

const fetchComTratamento = async () => {
  try {
    const response = await fetch('https://api.exemplo.com/recurso');
    if (!response.ok) {
      throw new Error(`Erro HTTP: ${response.status} - ${response.statusText}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Falha na requisição:', error);
    throw error;
  }
};

3. Trabalhando com Axios no React

Axios é uma biblioteca popular que abstrai o trabalho com requisições HTTP. Primeiro, instale-a:

npm install axios

Operações CRUD com axios são mais concisas:

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.exemplo.com',
  timeout: 5000
});

// GET
const usuarios = await api.get('/usuarios');

// POST
const novoUsuario = await api.post('/usuarios', { nome: 'Maria', email: 'maria@email.com' });

// PUT
const atualizado = await api.put('/usuarios/1', { nome: 'Maria Silva' });

// DELETE
await api.delete('/usuarios/1');

Uma das grandes vantagens do axios são os interceptors, que permitem interceptar requisições e respostas para adicionar headers, tokens ou logs automaticamente:

// Interceptor de requisição
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  console.log('Requisição enviada:', config);
  return config;
}, error => Promise.reject(error));

// Interceptor de resposta
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      console.log('Token expirado, redirecionando para login...');
      // Lógica de refresh token ou redirecionamento
    }
    return Promise.reject(error);
  }
);

4. Gerenciamento de estado das requisições com hooks

No React, usamos useState e useEffect para gerenciar o ciclo de vida das requisições:

import { useState, useEffect } from 'react';
import axios from 'axios';

function ListaUsuarios() {
  const [usuarios, setUsuarios] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsuarios = async () => {
      try {
        setLoading(true);
        const response = await axios.get('https://api.exemplo.com/usuarios');
        setUsuarios(response.data);
        setError(null);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsuarios();
  }, []);

  if (loading) return <div>Carregando...</div>;
  if (error) return <div>Erro: {error}</div>;
  return (
    <ul>
      {usuarios.map(usuario => (
        <li key={usuario.id}>{usuario.nome}</li>
      ))}
    </ul>
  );
}

Cancelamento de requisições é uma boa prática para evitar memory leaks em componentes desmontados:

// Com fetch usando AbortController
useEffect(() => {
  const controller = new AbortController();

  fetch('https://api.exemplo.com/dados', { signal: controller.signal })
    .then(response => response.json())
    .then(data => setDados(data))
    .catch(err => {
      if (err.name !== 'AbortError') setError(err.message);
    });

  return () => controller.abort();
}, []);

// Com axios usando CancelToken
useEffect(() => {
  const source = axios.CancelToken.source();

  axios.get('https://api.exemplo.com/dados', { cancelToken: source.token })
    .then(response => setDados(response.data))
    .catch(err => {
      if (!axios.isCancel(err)) setError(err.message);
    });

  return () => source.cancel('Componente desmontado');
}, []);

5. Padrões avançados de requisição no React

Custom hooks tornam o código mais reutilizável:

// Hook personalizado useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) throw new Error('Erro na requisição');
        const json = await response.json();
        setData(json);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Uso
function Componente() {
  const { data, loading, error } = useFetch('https://api.exemplo.com/dados');
  // ...
}

Requisições paralelas com Promise.all:

const [usuarios, posts] = await Promise.all([
  axios.get('/usuarios'),
  axios.get('/posts')
]);

Debouncing para buscas em tempo real:

function BuscaUsuario() {
  const [termo, setTermo] = useState('');
  const [resultados, setResultados] = useState([]);
  const timerRef = useRef(null);

  const handleChange = (e) => {
    const valor = e.target.value;
    setTermo(valor);

    clearTimeout(timerRef.current);
    timerRef.current = setTimeout(async () => {
      if (valor.length > 2) {
        const response = await axios.get(`/usuarios?busca=${valor}`);
        setResultados(response.data);
      }
    }, 500);
  };

  return (
    <div>
      <input value={termo} onChange={handleChange} placeholder="Buscar usuário..." />
      <ul>
        {resultados.map(r => <li key={r.id}>{r.nome}</li>)}
      </ul>
    </div>
  );
}

6. Tratamento de erros e feedback visual

Erros específicos merecem tratamento diferenciado:

const handleError = (error) => {
  if (error.response) {
    const { status, data } = error.response;
    switch (status) {
      case 400:
        return 'Dados inválidos. Verifique o formulário.';
      case 401:
        return 'Não autorizado. Faça login novamente.';
      case 404:
        return 'Recurso não encontrado.';
      case 500:
        return 'Erro interno do servidor. Tente novamente mais tarde.';
      default:
        return data?.message || 'Ocorreu um erro inesperado.';
    }
  } else if (error.request) {
    return 'Sem resposta do servidor. Verifique sua conexão.';
  } else {
    return 'Erro ao configurar a requisição.';
  }
};

Integração com react-toastify para notificações:

import { toast } from 'react-toastify';

const fetchDados = async () => {
  try {
    const response = await axios.get('/dados');
    toast.success('Dados carregados com sucesso!');
    return response.data;
  } catch (error) {
    toast.error(handleError(error));
    throw error;
  }
};

7. Comparação: fetch vs axios no React

Característica Fetch Axios
Sintaxe Mais verbosa, requer .json() manual Mais concisa, retorna JSON automaticamente
Tratamento de erros Precisa verificar response.ok Rejeita automaticamente em erros HTTP
Interceptors Não nativo Suporte completo
Timeout Implementação manual timeout na configuração
Cancelamento AbortController CancelToken
Tamanho Nativo (0 KB) ~14 KB (gzip)
Transformação Manual Automática de JSON

Quando usar cada um:
- Fetch: Projetos pequenos, quando você quer evitar dependências externas, ou quando precisa de controle total sobre a requisição.
- Axios: Projetos médios a grandes, quando produtividade é prioridade, ou quando recursos como interceptors e timeout são necessários.

8. Boas práticas e conclusão

Organize suas chamadas de API em serviços separados:

// services/usuarioService.js
import axios from 'axios';

const API_URL = process.env.REACT_APP_API_URL;

const usuarioService = {
  listar: () => axios.get(`${API_URL}/usuarios`),
  criar: (dados) => axios.post(`${API_URL}/usuarios`, dados),
  atualizar: (id, dados) => axios.put(`${API_URL}/usuarios/${id}`, dados),
  deletar: (id) => axios.delete(`${API_URL}/usuarios/${id}`),
};

export default usuarioService;

Use variáveis de ambiente para URLs de API (arquivo .env):

REACT_APP_API_URL=https://api.exemplo.com/v1

Segurança: nunca exponha tokens ou chaves secretas no código front-end. Use variáveis de ambiente e, idealmente, um backend proxy para operações sensíveis.

Dominar requisições HTTP no React com fetch e axios é essencial para construir aplicações modernas. Enquanto fetch oferece simplicidade e zero dependências, axios proporciona uma experiência mais produtiva com recursos avançados. A escolha depende do seu projeto — mas conhecer ambos amplia seu arsenal como desenvolvedor.

Referências