CodeForge

React 19 de Cero a Experto / Datos

useQuery: cargar datos en una línea

Teoría22 min20 XP

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:

antes-modulo-3.tsx
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—:

con-usequery.tsx
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:

query-con-parametro.tsx
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.