Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useCallback, useRef } from "react";
interface UseFetchOptions<T> {
/** Omitir la obtención inicial. Predeterminado: false */
skip?: boolean;
/** Transformar la respuesta antes de configurar datos */
transform?: (data: unknown) => T;
/** Opciones de init de fetch personalizadas (encabezados, método, etc.) */
init?: RequestInit;
/** Dependencias que desencadenan un refetch cuando cambian */
deps?: unknown[];
}
interface UseFetchReturn<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
/** Desencadenar manualmente un refetch */
refetch: () => void;
/** Cancelar la solicitud actual */
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;
// Cancelar cualquier solicitud en vuelo
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") {
// La solicitud fue cancelada, no actualizar el estado
return;
}
if (mountedRef.current) {
setError(err instanceof Error ? err : new Error(String(err)));
setIsLoading(false);
}
}
}, [url, skip, transform, init, ...deps]);
// Obtener en montar y cuando cambien las dependencias
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 };
}Cuándo usarlo: Necesitas un hook ligero para obtener datos en casos de uso simples, prototipos, o cuando SWR/TanStack Query es demasiado pesado. Para aplicaciones de producción con almacenamiento en caché, deduplicación y revalidación, prefiere una 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>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!data) return null;
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
<button onClick={refetch}>Actualizar</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>Cargando publicaciones...</p>;
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Omitir obtención hasta que el usuario actúe
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="Buscar..."
/>
<button onClick={() => setSearchTerm(query)}>Buscar</button>
{isLoading && <p>Buscando...</p>}
{data?.results.map((r, i) => <p key={i}>{r}</p>)}
</div>
);
}Lo que esto demuestra:
userId cambia, botón de refetch manualnull como URL para omitir la obtención hasta que el usuario envíe una búsquedaAbortController. Si la URL o las dependencias cambian antes de que se complete la solicitud, la solicitud anterior se cancela para prevenir condiciones de carrera.null como la URL omite la obtención completamente. Este es un patrón común para obtención condicional (similar a la obtención condicional de SWR).setData, permitiéndote remoldear respuestas de API (filtrar, mapear, seleccionar campos) sin estado adicional.mountedRef previene actualizaciones de estado después de desmontar, evitando advertencias de React.DOMException con name: "AbortError". El hook ignora silenciosamente estos para evitar estados de error falsos.| Parámetro | Tipo | Predeterminado | Descripción |
|---|---|---|---|
url | string or null | - | URL de fetch, o null para omitir |
options.skip | boolean | false | Omitir la obtención |
options.transform | (data: unknown) => T | - | Transformar datos de respuesta |
options.init | RequestInit | - | Opciones de fetch (encabezados, método, cuerpo) |
options.deps | unknown[] | [] | Deps adicionales que desencadenan refetch |
| Retorno | Tipo | Descripción |
|---|---|---|
data | T or null | Datos de respuesta, o null |
error | Error or null | Error, o null |
isLoading | boolean | Si una solicitud está en vuelo |
refetch | () => void | Desencadenar manualmente un nuevo fetch |
abort | () => void | Cancelar la solicitud actual |
Solicitudes POST/PUT: Pasa método y cuerpo a través de init:
const { data, isLoading } = useFetch("/api/users", {
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice" }),
},
});Con reintentos: Agrega reintentos automáticos en caso de falla:
// Dentro de fetchData, envuelve en un bucle de reintento:
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, { signal: controller.signal });
// ...manejo de éxito
break;
} catch (err) {
if (attempt === maxRetries - 1) throw err;
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}| Feature | useFetch (this hook) | SWR | TanStack Query |
|---|---|---|---|
| Tamaño del paquete | 0 KB | ~4 KB | ~13 KB |
| Almacenamiento en caché | No | Sí | Sí |
| Deduplicación | No | Sí | Sí |
| Revalidación | Solo manual | Automático | Automático |
| Actualizaciones optimistas | No | Sí | Sí |
| DevTools | No | No | Sí |
| Mejor para | Prototipos, casos simples | Complejidad media | Aplicaciones complejas |
T predeterminado a unknown y se infiere de la función transform si se proporciona.data se tipifica como T | null (null antes del primer fetch exitoso).error siempre es Error | null, envolviendo throws no-Error en new Error(String(err)).AbortController cancela solicitudes obsoletas; siempre úsalo.init en cada renderizado desencadena refetches infinitos. Fix: Memoiza init con useMemo o defínelo fuera del componente....deps en la matriz de dependencias puede causar refetches inesperados si deps contienen referencias inestables. Fix: Asegúrate de que deps contengan solo primitivos o referencias estables.| Paquete | Nombre del Hook | Notas |
|---|---|---|
swr | useSWR | Stale-while-revalidate, más popular |
@tanstack/react-query | useQuery | Completo, devtools, mutaciones |
react | use() | React 19 incorporado para fetching basado en suspense |
usehooks-ts | useFetch | Implementación simple similar |
axios + custom hook | - | Usa axios para interceptores, transforma con un hook envolvente |
fetchData crea un nuevo AbortController y cancela la solicitud anterior en vuelo.controller.signal.aborted previene que respuestas obsoletas actualicen el estado.Pasa skip: true o pasa null como la URL:
// Opción 1: opción skip
const { refetch } = useFetch("/api/data", { skip: true });
// Opción 2: URL null
const { data } = useFetch(query ? `/api/search?q=${query}` : null);transform se ejecuta antes de setData, permitiéndote remoldear la respuesta de la API (filtrar, mapear, seleccionar campos) sin estado adicional.Pasa método y cuerpo a través de la opción init:
const { data } = useFetch("/api/users", {
init: {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Alice" }),
},
});useCallback, desencadenando refetches infinitos.init con useMemo o defínelo como una constante fuera del componente.deps contiene objetos o matrices que se recrean en cada renderizado, el cambio de referencia desencadena una nueva función fetchData, que desencadena el efecto de fetch.DOMException con name: "AbortError".T predeterminado a unknown y puede proporcionarse explícitamente: useFetch<User>(url).transform, T se infiere de su tipo de retorno.data se tipifica como T | null, donde null representa el estado pre-fetch.error se tipifica como Error | null.new Error(String(err)) para normalizar el tipo.Usa la función abort del valor de retorno:
const { data, isLoading, abort } = useFetch("/api/slow-endpoint");
return <button onClick={abort}>Cancelar</button>;Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥