Skip to content

Tu Primera Query

¡Es hora de crear tu primera query con TanStack Query! En esta sección aprenderás los conceptos fundamentales creando ejemplos prácticos paso a paso.

Una query es una petición para obtener datos del servidor. En TanStack Query, cada query se identifica por:

  1. Query Key: Un identificador único (array o string)
  2. Query Function: La función que hace la petición al servidor
const { data, isLoading, error } = useQuery({
queryKey: ['posts'], // 👈 Identificador único
queryFn: () => fetchPosts() // 👈 Función que obtiene los datos
});

Vamos a crear una query simple para obtener una lista de posts:

src/components/PostsList.jsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';
function PostsList() {
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Error fetching posts');
}
return response.json();
}
});
if (isLoading) return <div>Cargando posts...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Lista de Posts</h2>
{posts?.map(post => (
<div key={post.id} style={{ margin: '20px 0', padding: '10px', border: '1px solid #ccc' }}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default PostsList;

TanStack Query proporciona varios estados útiles:

function PostsList() {
const {
data: posts, // Los datos obtenidos
isLoading, // Primera carga (sin datos en cache)
isFetching, // Cualquier petición en chapter
isError, // Si hay error
error, // El objeto error
isSuccess, // Si la petición fue exitosa
refetch // Función para recargar manualmente
} = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
});
// UI más granular basada en estados
if (isLoading) return <div>🔄 Cargando por primera vez...</div>;
if (isError) return <div>❌ Error: {error.message}</div>;
if (!posts) return <div>📭 No hay posts disponibles</div>;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>Lista de Posts</h2>
<button onClick={() => refetch()} disabled={isFetching}>
{isFetching ? '🔄 Recargando...' : '🔄 Recargar'}
</button>
</div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
// String simple
useQuery({
queryKey: 'posts', // ⚠️ No recomendado
queryFn: fetchPosts
});
// Array (recomendado)
useQuery({
queryKey: ['posts'], // ✅ Mejor práctica
queryFn: fetchPosts
});
// Con parámetros
useQuery({
queryKey: ['posts', { status: 'published' }],
queryFn: () => fetchPosts({ status: 'published' })
});
// Con ID específico
useQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId)
});
// Con múltiples parámetros
useQuery({
queryKey: ['posts', { page: 1, limit: 10, search: 'react' }],
queryFn: ({ queryKey }) => {
const [_key, params] = queryKey;
return fetchPosts(params);
}
});

Vamos a crear una query que acepta parámetros:

src/components/UserPosts.jsx
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
function UserPosts() {
const [userId, setUserId] = useState(1);
const {
data: posts,
isLoading,
error,
isFetching
} = useQuery({
queryKey: ['posts', 'user', userId],
queryFn: async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`
);
if (!response.ok) {
throw new Error('Error fetching user posts');
}
return response.json();
},
// Solo ejecutar si userId existe
enabled: !!userId
});
return (
<div>
<div>
<label>
Seleccionar Usuario:
<select
value={userId}
onChange={(e) => setUserId(Number(e.target.value))}
>
{[1, 2, 3, 4, 5].map(id => (
<option key={id} value={id}>Usuario {id}</option>
))}
</select>
</label>
{isFetching && <span> 🔄 Cargando...</span>}
</div>
{isLoading ? (
<div>Cargando posts del usuario...</div>
) : error ? (
<div>Error: {error.message}</div>
) : (
<div>
<h3>Posts del Usuario {userId}</h3>
{posts?.map(post => (
<div key={post.id} style={{ margin: '10px 0', padding: '10px', background: '#f5f5f5' }}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</div>
)}
</div>
);
}
export default UserPosts;
const { data } = useQuery({
queryKey: ['post', postId],
queryFn: ({ queryKey }) => {
const [_key, id] = queryKey; // Extraer parámetros del queryKey
return fetch(`/api/posts/${id}`).then(res => res.json());
}
});
const { data } = useQuery({
queryKey: ['posts'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/posts', { signal });
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
}
});
const fetchPostsWithDetails = async ({ queryKey, signal }) => {
const [_key, filters] = queryKey;
try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(filters),
signal, // Para cancelación automática
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Transformar datos si es necesario
return data.map(post => ({
...post,
createdAt: new Date(post.createdAt),
excerpt: post.body.substring(0, 100) + '...'
}));
} catch (error) {
if (error.name === 'AbortError') {
console.log('Query cancelled');
throw error;
}
throw new Error(`Failed to fetch posts: ${error.message}`);
}
};
function PostsWithDetails() {
const { data: posts, isLoading, error } = useQuery({
queryKey: ['posts', { status: 'published' }],
queryFn: fetchPostsWithDetails
});
// ... resto del componente
}
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Cache y actualización
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 10 * 60 * 1000, // 10 minutos
// Reintentos
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
// Refetch automático
refetchOnWindowFocus: false,
refetchOnReconnect: true,
refetchInterval: 30000, // Cada 30 segundos
// Ejecución condicional
enabled: true,
});
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// Inicializar con datos
initialData: [],
// Placeholder mientras carga
placeholderData: [],
// Transformar datos
select: (data) => data.filter(post => post.published),
// Callbacks
onSuccess: (data) => {
console.log('Posts cargados:', data.length);
},
onError: (error) => {
console.error('Error cargando posts:', error);
},
// Estructura de datos
structuralSharing: true,
});
src/components/PostsDashboard.jsx
import React, { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
const fetchPosts = async ({ queryKey }) => {
const [_key, filters] = queryKey;
const params = new URLSearchParams(filters);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?${params}`
);
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
};
function PostsDashboard() {
const [filters, setFilters] = useState({});
const [selectedUserId, setSelectedUserId] = useState('');
const {
data: posts = [],
isLoading,
error,
isFetching,
refetch
} = useQuery({
queryKey: ['posts', filters],
queryFn: fetchPosts,
staleTime: 2 * 60 * 1000, // 2 minutos
select: (data) => {
// Transformar y filtrar datos
return data
.sort((a, b) => b.id - a.id) // Ordenar por ID descendente
.map(post => ({
...post,
excerpt: post.body.substring(0, 100) + '...'
}));
}
});
const handleFilterChange = (userId) => {
setSelectedUserId(userId);
setFilters(userId ? { userId } : {});
};
if (isLoading) {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<div>🔄 Cargando posts...</div>
</div>
);
}
if (error) {
return (
<div style={{ color: 'red', padding: '20px' }}>
<h3>❌ Error al cargar posts</h3>
<p>{error.message}</p>
<button onClick={() => refetch()}>Reintentar</button>
</div>
);
}
return (
<div style={{ padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2>Dashboard de Posts {isFetching && '🔄'}</h2>
<button onClick={() => refetch()} disabled={isFetching}>
Actualizar
</button>
</div>
<div style={{ marginBottom: '20px' }}>
<label>
Filtrar por usuario:
<select
value={selectedUserId}
onChange={(e) => handleFilterChange(e.target.value)}
>
<option value="">Todos los usuarios</option>
{[1, 2, 3, 4, 5].map(id => (
<option key={id} value={id}>Usuario {id}</option>
))}
</select>
</label>
</div>
<div>
<p>📊 Total de posts: {posts.length}</p>
<div style={{ display: 'grid', gap: '15px' }}>
{posts.map(post => (
<div
key={post.id}
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
backgroundColor: '#f9f9f9'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '10px' }}>
<strong>Post #{post.id}</strong>
<span style={{ color: '#666' }}>Usuario {post.userId}</span>
</div>
<h3 style={{ margin: '10px 0' }}>{post.title}</h3>
<p style={{ color: '#666' }}>{post.excerpt}</p>
</div>
))}
</div>
</div>
</div>
);
}
export default PostsDashboard;

✅ Mejores Prácticas para tu Primera Query

Section titled “✅ Mejores Prácticas para tu Primera Query”
  1. Usa arrays para queryKey: ['posts'] en lugar de 'posts'
  2. Maneja todos los estados: isLoading, error, data
  3. Usa enabled para queries condicionales: enabled: !!userId
  4. Transforma datos con select: Para filtrar o modificar la respuesta
  5. Implementa manejo de errores: Tanto en la UI como en la queryFn
  6. Usa AbortController: Para cancelación automática de peticiones

¡Has creado tu primera query con TanStack Query! Ahora comprendes:

  • Cómo estructurar una query básica
  • La importancia de los query keys
  • Cómo manejar diferentes estados
  • Cómo usar parámetros en queries
  • Opciones de configuración básicas

Ahora que dominas los fundamentos de las queries, es hora de explorar el hook más importante: useQuery en detalle.

Próximo paso: useQuery