Skip to content

useQuery

useQuery es el hook más importante de TanStack Query. Se usa para obtener datos del servidor y gestionarlos automáticamente en el cache. En esta sección exploraremos todas sus capacidades.

const {
data, // Los datos obtenidos
isLoading, // Primera carga (no hay datos en cache)
isFetching, // Cualquier petición en chapter
isError, // Si hay un error
error, // El objeto error
isSuccess, // Si la petición fue exitosa
refetch, // Función para recargar manualmente
// ... más propiedades
} = useQuery({
queryKey, // Identificador único
queryFn, // Función que obtiene los datos
// ... opciones
});
function UserProfile({ userId }) {
const { data, isLoading, isFetching, isError, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// isLoading: true solo en la primera carga (sin datos en cache)
if (isLoading) return <div>🔄 Cargando usuario...</div>;
// isError: true si hay algún error
if (isError) return <div>❌ Error: {error.message}</div>;
// data: undefined hasta que se cargan los datos
if (!data) return <div>📭 Usuario no encontrado</div>;
return (
<div>
<h2>{data.name} {isFetching && '🔄'}</h2>
<p>{data.email}</p>
{/* isFetching: true durante cualquier petición (incluso refetch) */}
</div>
);
}
function AdvancedUserProfile({ userId }) {
const {
data,
isLoading, // Primera carga
isFetching, // Cualquier fetch
isStale, // Datos obsoletos
isPlaceholderData, // Si se están mostrando datos placeholder
isPreviousData, // Si se muestran datos de una query anterior
isIdle, // Query deshabilitada
status, // 'idle' | 'loading' | 'error' | 'success'
fetchStatus, // 'idle' | 'fetching' | 'paused'
failureCount, // Número de reintentos fallidos
refetch,
remove // Función para eliminar query del cache
} = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
});
// UI granular basada en estados
if (status === 'idle') return <div>⏳ Query deshabilitada</div>;
if (isLoading) return <div>🔄 Cargando por primera vez...</div>;
if (isError) return <div>❌ Error después de {failureCount} intentos</div>;
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<h2>{data.name}</h2>
{isStale && <span style={{ color: 'orange' }}>📡 Datos obsoletos</span>}
{isFetching && <span>🔄 Actualizando...</span>}
{isPreviousData && <span style={{ color: 'blue' }}>👻 Datos anteriores</span>}
</div>
<p>{data.email}</p>
<div>
<button onClick={() => refetch()}>Actualizar</button>
<button onClick={() => remove()}>Limpiar Cache</button>
</div>
</div>
);
}
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Control de cache
staleTime: 5 * 60 * 1000, // 5 min - tiempo hasta marcar como obsoleto
cacheTime: 10 * 60 * 1000, // 10 min - tiempo en cache después de unmount
// Comportamiento de refetch
refetchOnWindowFocus: true, // Refetch al enfocar ventana
refetchOnReconnect: true, // Refetch al reconectar internet
refetchOnMount: true, // Refetch al montar componente
refetchInterval: 30000, // Refetch cada 30 segundos
refetchIntervalInBackground: false, // Solo refetch si ventana está enfocada
});
function UserPosts({ userId, isVisible }) {
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(userId),
// Solo ejecutar si userId existe y el componente es visible
enabled: !!userId && isVisible,
// También puedes usar función
enabled: () => {
return userId && userId > 0 && isVisible;
}
});
return (
<div>
{posts?.map(post => <div key={post.id}>{post.title}</div>)}
</div>
);
}
const { data } = useQuery({
queryKey: ['api-data'],
queryFn: fetchApiData,
// Configuración de reintentos
retry: 3, // Número de reintentos
retry: (failureCount, error) => {
// Lógica personalizada de retry
if (error.status === 404) return false; // No reintentar 404
if (error.status >= 500) return failureCount < 3; // Solo errores del servidor
return false;
},
// Delay entre reintentos (backoff exponencial)
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
// Callbacks de error
onError: (error) => {
console.error('Query failed:', error);
// Enviar a servicio de logging
// Mostrar notificación
}
});
const { data: publishedPosts } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Transformar datos antes de devolverlos
select: (data) => {
return data
.filter(post => post.published) // Solo publicados
.sort((a, b) => new Date(b.date) - new Date(a.date)) // Más recientes primero
.map(post => ({ // Transformar estructura
...post,
excerpt: post.content.substring(0, 100) + '...',
formattedDate: new Date(post.date).toLocaleDateString()
}));
}
});

Cuando una query depende del resultado de otra:

function UserPostsAndComments({ userId }) {
// Primera query: obtener usuario
const { data: user, isLoading: userLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
});
// Segunda query: obtener posts del usuario (depende de la primera)
const { data: posts, isLoading: postsLoading } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchUserPosts(user.id),
enabled: !!user?.id // Solo ejecutar si tenemos el usuario
});
// Tercera query: obtener comentarios del primer post
const { data: comments } = useQuery({
queryKey: ['comments', posts?.[0]?.id],
queryFn: () => fetchPostComments(posts[0].id),
enabled: !!posts?.[0]?.id
});
if (userLoading) return <div>Cargando usuario...</div>;
if (postsLoading) return <div>Cargando posts...</div>;
return (
<div>
<h2>{user.name}</h2>
<div>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
{post.id === posts[0]?.id && comments && (
<div>
<h4>Comentarios:</h4>
{comments.map(comment => (
<p key={comment.id}>{comment.body}</p>
))}
</div>
)}
</div>
))}
</div>
</div>
);
}
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
// Datos iniciales (se consideran fresh)
initialData: {
id: userId,
name: 'Usuario Cargando...',
email: ''
},
// O función que devuelve datos iniciales
initialData: () => {
// Intentar obtener datos del cache de otra query
return queryClient.getQueryData(['users'])
?.find(user => user.id === userId);
}
});
const { data: posts, isPlaceholderData } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Datos placeholder (se consideran stale)
placeholderData: [
{ id: 1, title: 'Cargando...', body: 'Este post se está cargando...' },
{ id: 2, title: 'Cargando...', body: 'Este post se está cargando...' }
],
// O función para generar placeholder
placeholderData: () => {
return Array.from({ length: 5 }, (_, i) => ({
id: i + 1,
title: `Post ${i + 1} cargando...`,
body: 'Contenido cargando...'
}));
}
});
// UI que muestra si son datos placeholder
return (
<div>
{isPlaceholderData && <div>📡 Cargando datos reales...</div>}
{posts?.map(post => (
<div key={post.id} style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Callbacks durante el ciclo de vida
onSuccess: (data) => {
console.log('✅ Posts cargados exitosamente:', data.length);
// Guardar en localStorage
localStorage.setItem('lastPostsCount', data.length);
// Enviar analytics
analytics.track('posts_loaded', { count: data.length });
},
onError: (error) => {
console.error('❌ Error cargando posts:', error);
// Mostrar notificación de error
toast.error(`Error: ${error.message}`);
// Enviar error a servicio de logging
errorLogger.log(error);
},
onSettled: (data, error) => {
// Se ejecuta siempre, con éxito o error
console.log('🏁 Query completada');
// Limpiar loading states globales
setGlobalLoading(false);
}
});

🧪 Ejemplo Avanzado: Búsqueda con Debounce

Section titled “🧪 Ejemplo Avanzado: Búsqueda con Debounce”
import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useDebounce } from 'use-debounce'; // npm install use-debounce
function SearchPosts() {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedSearchTerm] = useDebounce(searchTerm, 300);
const {
data: posts = [],
isLoading,
isFetching,
error,
refetch
} = useQuery({
queryKey: ['posts', 'search', debouncedSearchTerm],
queryFn: async () => {
if (!debouncedSearchTerm) return [];
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?title_like=${debouncedSearchTerm}`
);
if (!response.ok) {
throw new Error('Error en búsqueda');
}
return response.json();
},
// Solo buscar si hay término de búsqueda
enabled: !!debouncedSearchTerm && debouncedSearchTerm.length >= 2,
// Configuración para búsqueda
staleTime: 5 * 60 * 1000, // 5 minutos
keepPreviousData: true, // Mantener datos anteriores mientras carga
// Callbacks
onSuccess: (data) => {
console.log(`Encontrados ${data.length} posts para "${debouncedSearchTerm}"`);
}
});
// Memoizar estadísticas para evitar recálculos
const stats = useMemo(() => ({
totalResults: posts.length,
isSearching: searchTerm !== debouncedSearchTerm,
hasResults: posts.length > 0
}), [posts.length, searchTerm, debouncedSearchTerm]);
return (
<div style={{ padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
placeholder="Buscar posts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<div style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>
{stats.isSearching && '⌨️ Escribiendo...'}
{isFetching && '🔍 Buscando...'}
{!isFetching && debouncedSearchTerm && (
<span>
📊 {stats.totalResults} resultados para "{debouncedSearchTerm}"
</span>
)}
</div>
</div>
{error && (
<div style={{ color: 'red', margin: '10px 0' }}>
❌ Error: {error.message}
<button onClick={() => refetch()} style={{ marginLeft: '10px' }}>
Reintentar
</button>
</div>
)}
{isLoading && (
<div style={{ textAlign: 'center', padding: '20px' }}>
🔄 Buscando posts...
</div>
)}
{!isLoading && stats.hasResults && (
<div>
{posts.map(post => (
<div
key={post.id}
style={{
margin: '10px 0',
padding: '15px',
border: '1px solid #eee',
borderRadius: '8px',
backgroundColor: '#f9f9f9'
}}
>
<h3 style={{ margin: '0 0 10px 0' }}>{post.title}</h3>
<p style={{ margin: 0, color: '#666' }}>{post.body}</p>
</div>
))}
</div>
)}
{!isLoading && debouncedSearchTerm && !stats.hasResults && (
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
📭 No se encontraron posts para "{debouncedSearchTerm}"
</div>
)}
</div>
);
}
export default SearchPosts;
  1. Usa arrays para queryKey: ['posts', id] es mejor que 'posts-' + id
  2. Manten queryKeys consistentes: Define tus claves en un lugar centralizado
  3. Usa enabled para queries condicionales: Evita peticiones innecesarias
  4. Transforma datos con select: Para filtrar o modificar respuestas
  5. Configura staleTime apropiadamente: Basado en qué tan frecuentemente cambian tus datos
  6. Maneja todos los estados: isLoading, error, data
  7. Usa keepPreviousData para listas: Para mejor UX en paginación/filtros
  8. Implementa retry inteligente: No reintentar errores 4xx
  9. Aprovecha el cache: Queries con mismas keys comparten datos
  10. Usa callbacks para side effects: onSuccess, onError, onSettled

¡Ahora dominas useQuery! Es momento de aprender sobre las mutaciones para modificar datos en el servidor.

Próximo paso: useMutation