Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useCallback, useRef } from "react";
interface UseFetchOptions<T> {
/** Pula a busca inicial. Padrão: false */
skip?: boolean;
/** Transforma a resposta antes de definir os dados */
transform?: (data: unknown) => T;
/** Opções customizadas de init do fetch (cabeçalhos, método, etc.) */
init?: RequestInit;
/** Dependências que disparam uma re-busca quando alteradas */
deps?: unknown[];
}
interface UseFetchReturn<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
/** Dispara manualmente uma re-busca */
refetch: () => void;
/** Aborta a requisição atual */
abort: () => void;
}
function useFetch<T = unknown>(
url: string | null,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const { skip = false, transform, init, deps = [] } = options;
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isLoading, setIsLoading] = useState(!skip && url !== null);
const abortControllerRef = useRef<AbortController | null>(null);
const mountedRef = useRef(true);
const fetchData = useCallback(async () => {
if (!url || skip) return;
// Aborta qualquer requisição em andamento
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
setIsLoading(true);
setError(null);
try {
const response = await fetch(url, {
...init,
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const json = await response.json();
const result = transform ? transform(json) : (json as T);
if (mountedRef.current && !controller.signal.aborted) {
setData(result);
setIsLoading(false);
}
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") {
// Requisição foi abortada, não atualiza o estado
return;
}
if (mountedRef.current) {
setError(err instanceof Error ? err : new Error(String(err)));
setIsLoading(false);
}
}
}, [url, skip, transform, init, ...deps]);
// Busca ao montar e quando as dependências mudam
useEffect(() => {
mountedRef.current = true;
fetchData();
return () => {
mountedRef.current = false;
abortControllerRef.current?.abort();
};
}, [fetchData]);
const abort = useCallback(() => {
abortControllerRef.current?.abort();
setIsLoading(false);
}, []);
return { data, error, isLoading, refetch: fetchData, abort };
}Quando usar isso: Você precisa de um hook leve para buscar dados para casos simples, protótipos, ou quando SWR/TanStack Query é muito pesado. Para aplicações em produção com cache, desduplicação e revalidação, prefira uma biblioteca dedicada.
"use client";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, error, isLoading, refetch } = useFetch<User>(
`https://jsonplaceholder.typicode.com/users/${userId}`,
{ deps: [userId] }
);
if (isLoading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error.message}</p>;
if (!data) return null;
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
<button onClick={refetch}>Atualizar</button>
</div>
);
}
function PostList() {
const { data: posts, isLoading } = useFetch<
Array<{ id: number; title: string }>
>("https://jsonplaceholder.typicode.com/posts", {
transform: (raw) =>
(raw as Array<{ id: number; title: string }>).slice(0, 10),
});
if (isLoading) return <p>Carregando posts...</p>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Pula a busca até o usuário agir
function SearchResults() {
const [query, setQuery] = useState("");
const [searchTerm, setSearchTerm] = useState<string | null>(null);
const { data, isLoading } = useFetch<{ results: string[] }>(
searchTerm
? `https://api.example.com/search?q=${encodeURIComponent(searchTerm)}`
: null
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pesquisar..."
/>
<button onClick={() => setSearchTerm(query)}>Pesquisar</button>
{isLoading && <p>Pesquisando...</p>}
{data?.results.map((r, i) => <p key={i}>{r}</p>)}
</div>
);
}O que isso demonstra:
userId muda, botão de re-busca manual.null como URL para pular a busca até o usuário enviar uma pesquisa.AbortController. Se a URL ou as dependências mudarem antes da requisição completar, a requisição anterior é abortada para prevenir condições de corrida.null como URL pula a busca completamente. Este é um padrão comum para busca condicional (similar à busca condicional do SWR).transform: Executada antes de setData, permitindo que você reestruture respostas de API (filtrar, mapear, selecionar campos) sem estado extra.mountedRef previne atualizações de estado após o desmontar, evitando avisos do React.AbortError: Requisições abortadas lançam uma DOMException com name: "AbortError". O hook ignora silenciosamente estas para evitar estados de erro falsos.| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
url | string ou null | - | URL da busca, ou null para pular |
options.skip | boolean | false | Pula a busca |
options.transform | (data: unknown) => T | - | Transforma os dados da resposta |
options.init | RequestInit | - | Opções do fetch (cabeçalhos, método, corpo) |
options.deps | unknown[] | [] | Dependências extras que disparam re-busca |
| Retorno | Tipo | Descrição |
|---|---|---|
data | T ou null | Dados da resposta, ou null |
error | Error ou null | Erro, ou null |
isLoading | boolean | Se uma requisição está em andamento |
refetch | () => void | Dispara manualmente uma nova busca |
abort | () => void | Cancela a requisição atual |
Requisições POST/PUT: Passe o método e o corpo através de init:
const { data, isLoading } = useFetch("/api/users", {
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice" }),
},
});Com retentativas: Adicione retentativas automáticas em caso de falha:
// Dentro de fetchData, envolva em um loop de retentativas:
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, { signal: controller.signal });
// ... tratamento de sucesso
break;
} catch (err) {
if (attempt === maxRetries - 1) throw err;
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}| Funcionalidade | useFetch (este hook) | SWR | TanStack Query |
|---|---|---|---|
| Tamanho do bundle | 0 KB | ~4 KB | ~13 KB |
| Cache | Não | Sim | Sim |
| Desduplicação | Não | Sim | Sim |
| Revalidação | Apenas manual | Automática | Automática |
| Atualizações otimistas | Não | Sim | Sim |
| DevTools | Não | Não | Sim |
| Ideal para | Protótipos, casos simples | Complexidade média | Aplicações complexas |
T tem o padrão unknown e é inferido da função transform se fornecida.data é tipado como T | null (null antes da primeira busca bem-sucedida).error é sempre Error | null, envolvendo lançamentos que não são Error em new Error(String(err)).AbortController cancela requisições obsoletas; sempre use-o.init - Passar um novo objeto init a cada renderização dispara re-buscas infinitas. Correção: Memoize init com useMemo ou defina-o fora do componente.deps em useCallback - O spread ...deps no array de dependências pode causar re-buscas inesperadas se deps contiver referências instáveis. Correção: Certifique-se de que deps contenha apenas primitivos ou referências estáveis.| Pacote | Nome do Hook | Notas |
|---|---|---|
swr | useSWR | Stale-while-revalidate, o mais popular |
@tanstack/react-query | useQuery | Completo, devtools, mutações |
react | use() | Integrado no React 19 para busca baseada em suspense |
usehooks-ts | useFetch | Implementação simples similar |
axios + hook customizado | - | Use axios para interceptadores, transforme com um hook wrapper |
fetchData cria um novo AbortController e aborta a requisição anterior em andamento.controller.signal.aborted previne que respostas obsoletas atualizem o estado.Passe skip: true ou null como URL:
// Opção 1: opção skip
const { refetch } = useFetch("/api/data", { skip: true });
// Opção 2: URL nula
const { data } = useFetch(query ? `/api/search?q=${query}` : null);transform é executado antes de setData, permitindo que você reestruture a resposta da API (filtrar, mapear, selecionar campos) sem estado extra.Passe o método e o corpo através da opção init:
const { data } = useFetch("/api/users", {
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice" }),
},
});useCallback, disparando re-buscas infinitas.init com useMemo ou defina-o como uma constante fora do componente.deps contiver objetos ou arrays que são recriados a cada renderização, a mudança de referência dispara uma nova função fetchData, que dispara o efeito de busca.deps contenha apenas primitivos ou referências estáveis.DOMException com name: "AbortError".T tem o padrão unknown e pode ser explicitamente fornecido: useFetch<User>(url).transform for fornecida, T é inferido do seu tipo de retorno.data é tipado como T | null, onde null representa o estado pré-busca.error é tipado como Error | null.Error são envolvidos em new Error(String(err)) para normalizar o tipo.Use a função abort do valor de retorno:
const { data, isLoading, abort } = useFetch("/api/slow-endpoint");
return <button onClick={abort}>Cancelar</button>;Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥