React 19 de Cero a Experto / Datos
useQuery: cargar datos en una línea
Todo el patrón del módulo 3 —tres estados, el efecto, la bandera contra la race condition, el manejo
de error— se reduce con TanStack Query a una línea: useQuery. Le das una CLAVE (para la caché) y
una FUNCIÓN que obtiene los datos, y él te devuelve data, isLoading, isError ya gestionados,
con caché y deduplicación de regalo. Hoy dominas el hook central de la librería.
La comanda con número de ticket
Antes y después
Compara el patrón del módulo 3 con useQuery. Primero, lo que ya sabes hacer a mano:
function Gastos() {
const [gastos, setGastos] = useState<Gasto[]>([]);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let ignorar = false;
setCargando(true);
fetch("/api/gastos")
.then((r) => r.json())
.then((d) => { if (!ignorar) { setGastos(d); setCargando(false); } })
.catch((e) => { if (!ignorar) { setError(e.message); setCargando(false); } });
return () => { ignorar = true; };
}, []);
if (cargando) return <p>Cargando…</p>;
if (error) return <p>{error}</p>;
return <ul>{gastos.map((g) => <li key={g.id}>{g.nombre}</li>)}</ul>;
}Ahora, lo mismo con useQuery —y encima con caché, dedupe y reintentos que el de arriba no tenía—:
import { useQuery } from "@tanstack/react-query";
async function obtenerGastos(): Promise<Gasto[]> {
const res = await fetch("/api/gastos");
if (!res.ok) throw new Error(`HTTP ${res.status}`); // lanzar = 'isError'
return res.json();
}
function Gastos() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ["gastos"], // la CLAVE de caché
queryFn: obtenerGastos, // CÓMO obtener los datos
});
if (isLoading) return <p>Cargando…</p>;
if (isError) return <p>{error.message}</p>;
return <ul>{data.map((g) => <li key={g.id}>{g.nombre}</li>)}</ul>;
}Desaparecen los tres useState, el useEffect, la bandera ignorar y el manejo manual. useQuery
te da data, isLoading, isError y error ya gestionados. Y a diferencia del "antes", esta
versión cachea por ["gastos"], deduplica peticiones simultáneas y reintenta si la red falla —todo
sin que escribas una línea extra—.
Las dos piezas: queryKey y queryFn
Datos por parámetro
Cuando los datos dependen de una variable (un id, un filtro), va en la queryKey Y en la queryFn:
function DetalleGasto({ id }: { id: number }) {
const { data, isLoading } = useQuery({
queryKey: ["gasto", id], // la clave incluye el id
queryFn: () => obtenerGasto(id), // la función lo usa
});
if (isLoading) return <p>Cargando…</p>;
return <p>{data?.nombre}: {data?.valor}</p>;
}Al meter id en la queryKey, cada gasto tiene su propia entrada de caché: ["gasto", 1],
["gasto", 2]… Si cambias de id, TanStack busca en caché esa clave (si ya la tiene, la muestra al
instante mientras revalida) o la carga. Por eso la queryKey debe incluir TODO lo que la queryFn
usa para obtener los datos —igual que las dependencias de un useEffect—.
¿Para qué sirven la queryKey y la queryFn en useQuery?
Mini-reto
En tu proyecto con TanStack Query instalada (o razonando la API): 1) escribe una queryFn
obtenerUsuario(id) que haga fetch a una API y lance si !res.ok; 2) úsala en un useQuery con
queryKey: ["usuario", id]; 3) renderiza los tres estados (isLoading, isError, data).
Pregúntate: si dos componentes distintos usan useQuery con la clave ["usuario", 5], ¿cuántas
peticiones se hacen? (Respuesta: una —la comparten—.)
Qué sigue
useQuery carga y cachea. Pero surge la pregunta: ¿cuándo considera TanStack que los datos están
"viejos" y hay que refrescarlos? Y cuando TÚ sabes que cambiaron (porque agregaste algo), ¿cómo le
dices que recargue? La próxima lección abre la caché: staleTime, revalidación automática y la
invalidación manual.