El problema de useEffect + fetch a mano
Cargar datos del servidor parece sencillo: un useEffect que hace fetch y
guarda el resultado en estado. Es lo primero que aprendemos, y para un caso
aislado funciona:
function Perfil({ userId }) {
const [usuario, setUsuario] = useState(null);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelado = false;
setCargando(true);
fetch(`/api/usuarios/${userId}`)
.then((r) => {
if (!r.ok) throw new Error("Error " + r.status);
return r.json();
})
.then((data) => { if (!cancelado) setUsuario(data); })
.catch((e) => { if (!cancelado) setError(e); })
.finally(() => { if (!cancelado) setCargando(false); });
return () => { cancelado = true; };
}, [userId]);
// ...renderizado según cargando / error / usuario
}
Mira cuánto código de fontanería hay para una sola petición, y eso que aún le faltan cosas. Cuando la app crece, este patrón se queda corto porque cada pantalla tiene que reinventar:
- Caché: si dos componentes piden
/api/usuarios/7, se hacen dos peticiones. No hay memoria compartida de lo ya descargado. - Deduplicación: varias llamadas simultáneas a la misma URL deberían fusionarse en una sola petición en vuelo.
- Revalidación: los datos caducan. ¿Cuándo se vuelven a pedir? ¿Al reenfocar la ventana, al reconectar la red, cada cierto tiempo?
- Estados:
isLoading,error,isFetching(refetch en segundo plano), datos antiguos mientras llegan los nuevos... gestionarlos a mano es frágil. - Cancelación y condiciones de carrera: si
userIdcambia rápido, una respuesta lenta puede pisar a una rápida (de ahí el flagcancelado).
Estos problemas no son del estado del cliente (lo que el usuario teclea o selecciona): son del estado del servidor, datos que viven en remoto, los cacheamos en el cliente y pueden quedar desactualizados. Las librerías de data fetching nacen para gestionar exactamente eso.
TanStack Query (React Query)
TanStack Query trata el estado del servidor como un recurso con caché. La
pieza central es el hook useQuery:
import { useQuery } from "@tanstack/react-query";
function Perfil({ userId }) {
const { data, isLoading, error } = useQuery({
queryKey: ["usuario", userId],
queryFn: () =>
fetch(`/api/usuarios/${userId}`).then((r) => r.json()),
staleTime: 60_000, // datos "frescos" durante 60 s
});
if (isLoading) return <p>Cargando…</p>;
if (error) return <p>Algo salió mal</p>;
return <h1>{data.nombre}</h1>;
}
Las claves:
queryKey(["usuario", userId]): identifica la consulta en la caché. Dos componentes con la misma key comparten los mismos datos y se hace una sola petición (deduplicación). SiuserIdcambia, la key cambia y se pide de nuevo.queryFn: la función que devuelve una promesa con los datos. Solo te preocupas de cómo pedir, no de cuándo ni de cachear.data/isLoading/error: los estados ya resueltos por ti. SinuseStateniuseEffectmanuales.staleTime: cuánto tiempo se consideran frescos los datos. Mientras lo estén, no se vuelven a pedir; pasado ese tiempo quedan stale y se revalidan (por ejemplo al reenfocar la ventana), mostrando entretanto los datos antiguos.refetch: la propia query expone una funciónrefetch()para volver a pedir bajo demanda (un botón "Actualizar").
Todo lo que antes escribías a mano —caché, deduplicación, revalidación, cancelación— viene de fábrica.
SWR
SWR (de Vercel) sigue la misma filosofía con una API más minimalista. Su nombre viene de stale-while-revalidate: muestra los datos cacheados (stale) al instante y, en paralelo, revalida en segundo plano.
import useSWR from "swr";
const fetcher = (url) => fetch(url).then((r) => r.json());
function Perfil({ userId }) {
const { data, error, isLoading } = useSWR(
`/api/usuarios/${userId}`,
fetcher
);
if (isLoading) return <p>Cargando…</p>;
if (error) return <p>Algo salió mal</p>;
return <h1>{data.nombre}</h1>;
}
Aquí la clave de caché es la propia URL (primer argumento) y el fetcher es
la función de descarga. SWR también deduplica peticiones, revalida al reenfocar
y reconectar, y expone mutate() para actualizar la caché.
Idea clave: separa el estado del servidor (datos remotos cacheados → TanStack Query / SWR) del estado del cliente (UI local →
useState, Zustand...). No metas datos del servidor en un store global "a mano"; deja que una librería de queries gestione su caché y su frescura.
(Estas librerías se instalan aparte; aquí las estudiamos de forma conceptual. En los ejercicios validables seguimos usando los hooks nativos de React.)