En esta página
Fetching de Datos con TanStack Query
El problema con useEffect para fetching
Hacer fetch en useEffect tiene varios problemas: condiciones de carrera, reintentos manuales, caché ausente, y mucho código boilerplate. TanStack Query resuelve todos estos problemas:
// Antes: fetching manual con useEffect
function ListaUsuarios() {
const [usuarios, setUsuarios] = useState([]);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Gestión manual de 20+ líneas de código...
}, []);
}
// Con TanStack Query: limpio y con todas las features
function ListaUsuarios() {
const { data, isLoading, error } = useQuery({
queryKey: ['usuarios'],
queryFn: () => fetch('/api/usuarios').then(r => r.json()),
});
}Instalación y configuración
npm install @tanstack/react-query @tanstack/react-query-devtools// main.tsx — configuración global
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 minuto de datos frescos
retry: 2, // 2 reintentos en error
refetchOnWindowFocus: true, // Refetch al volver a la pestaña
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);useQuery — fetching declarativo
interface Producto {
id: number;
nombre: string;
precio: number;
categoria: string;
}
function CatalogoProductos({ categoria }: { categoria: string }): React.JSX.Element {
const {
data: productos,
isLoading,
isError,
error,
isFetching, // true cuando hay un refetch en background
dataUpdatedAt,
} = useQuery({
queryKey: ['productos', categoria], // Caché separado por categoría
queryFn: async (): Promise<Producto[]> => {
const res = await fetch(`/api/productos?categoria=${categoria}`);
if (!res.ok) throw new Error(`Error ${res.status}: ${res.statusText}`);
return res.json() as Promise<Producto[]>;
},
staleTime: 5 * 60 * 1000,
select: (data) => data.sort((a, b) => a.precio - b.precio), // Transformar datos
});
if (isLoading) return <p>Cargando catálogo…</p>;
if (isError) return <p>Error: {error.message}</p>;
return (
<div>
{isFetching && <span>Actualizando…</span>}
<p>Actualizado: {new Date(dataUpdatedAt).toLocaleTimeString('es-ES')}</p>
<ul>
{productos?.map((p) => (
<li key={p.id}>{p.nombre} — ${p.precio}</li>
))}
</ul>
</div>
);
}Queries dependientes
function PerfilConPosts({ usuarioId }: { usuarioId: number }): React.JSX.Element {
const { data: usuario } = useQuery({
queryKey: ['usuario', usuarioId],
queryFn: () => fetch(`/api/usuarios/${usuarioId}`).then(r => r.json()) as Promise<Usuario>,
});
// Esta query solo se ejecuta cuando usuario?.id existe
const { data: posts } = useQuery({
queryKey: ['posts', usuario?.id],
queryFn: () => fetch(`/api/posts?autorId=${usuario!.id}`).then(r => r.json()) as Promise<Post[]>,
enabled: !!usuario?.id, // La query está deshabilitada hasta que tengamos el usuario
});
return (
<div>
<h2>{usuario?.nombre}</h2>
<p>{posts?.length ?? 0} posts publicados</p>
</div>
);
}Prefetching — cargar datos anticipadamente
function ListaArticulos(): React.JSX.Element {
const cliente = useQueryClient();
const { data: articulos } = useQuery({
queryKey: ['articulos'],
queryFn: obtenerArticulos,
});
const prefetchArticulo = async (id: number) => {
// Precargar el artículo cuando el usuario pasa el cursor
await cliente.prefetchQuery({
queryKey: ['articulo', id],
queryFn: () => obtenerArticuloPorId(id),
staleTime: 10 * 60 * 1000,
});
};
return (
<ul>
{articulos?.map((a) => (
<li key={a.id} onMouseEnter={() => void prefetchArticulo(a.id)}>
<Link to={`/articulos/${a.id}`}>{a.titulo}</Link>
</li>
))}
</ul>
);
}Gestión del caché con invalidaciones
function AdminPanel(): React.JSX.Element {
const cliente = useQueryClient();
const recargarTodo = async () => {
// Invalida e inmediatamente refetch todas las queries
await cliente.invalidateQueries();
};
const recargarProductos = async () => {
// Solo invalida las queries de productos (cualquier queryKey que empiece con 'productos')
await cliente.invalidateQueries({ queryKey: ['productos'] });
};
const limpiarCache = () => {
// Elimina todas las entradas del caché
cliente.clear();
};
return (
<div>
<button type="button" onClick={() => void recargarTodo()}>Recargar todo</button>
<button type="button" onClick={() => void recargarProductos()}>Recargar productos</button>
<button type="button" onClick={limpiarCache}>Limpiar caché</button>
</div>
);
}TanStack Query es hoy el estándar de la industria para fetching en aplicaciones React. Su sistema de caché, reintentos, sincronización en background y DevTools lo hacen indispensable en cualquier proyecto serio.
queryKey es la clave del sistema de caché de TanStack Query
El queryKey actúa como identificador único de los datos en caché. Arrays como ['posts', userId] hacen que posts del usuario 1 y usuario 2 sean entradas separadas. Al invalidar ['posts'], se invalidan todas las queries que empiezan con esa clave.
staleTime vs gcTime — entiende la diferencia
staleTime controla cuánto tiempo los datos se consideran frescos (no se refetch en background). gcTime controla cuánto tiempo los datos inactivos permanecen en caché antes de eliminarse. Un staleTime alto reduce las peticiones; un gcTime alto mantiene el caché más tiempo.
No olvides configurar QueryClientProvider en la raíz
TanStack Query requiere que QueryClientProvider envuelva toda la app (o la parte que usa queries). Sin él, useQuery lanzará un error. Crea un QueryClient fuera del componente para evitar recrearlo en cada render.
Inicia sesión para guardar tu progreso