Interstellar Code
Uso de TanStack React Query
Guía de uso para aprender a utilizar la librería de TanStack React Query para la gestión asíncrona de los datos en las aplicaciones de React, esta poderosa librería nos permite manejar el estado de los datos que obtenemos de forma asíncrona de una manera eficiente y sencilla, permitiéndonos abstraer temas tan complejos como el manejo del cache, revalidaciones de información, mutación de datos, etc.
29 de agosto de 2025
Tabla de contenido:
Uso de TanStack React Query
TanStack React Query es una librería para la gestión de datos asíncronos en aplicaciones web, esta librería tiene opciones para ser utilizada en multiples frameworks y librerías, pero donde más ha destacado es en su uso con aplicaciones de React, ya que nos permite manejar el estado de los datos obtenidos de forma asíncrona de una manera sencilla y eficiente, abstrae muchos de los conceptos complejos que conlleva el manejo de datos asíncronos como el cache, revalidaciones, mutaciones, etc.
Para comenzar a utilizar esta librería, primero debemos instalarla en nuestro proyecto:
npm install @tanstack/react-query
También es recomendado si estamos trabajando con algún linter para el código como por ejemplo ESLint, instalar el plugin oficial para React Query:
npm install -D @tanstack/eslint-plugin-query
Además de esto también podemos instalar el paquete de Devtools para React Query, que nos permitirá visualizar el estado de nuestras queries y mutations en tiempo real, lo cual es muy útil para el desarrollo y debugging de nuestra aplicación:
npm install @tanstack/react-query-devtools
Una vez que ya hemos instalado la librería, lo primero que debemos hacer es configurar el QueryClient
y el QueryClientProvider
en la raíz de nuestra aplicación, esto nos permitirá utilizar React Query en cualquier parte de nuestra aplicación:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const client = new QueryClient()
function App() {
return (
<QueryClientProvider client={client}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Queries
Las queries son la forma principal de obtener datos usando React Query, para crear una query debemos utilizar el hook useQuery
, este hook recibe un objeto con dos propiedades principales, una clave única para la query y una función que retorna una promesa con los datos que queremos obtener. Además de estas dos propiedades, el hook useQuery
también acepta otras opciones para configurar el comportamiento de la query, como por ejemplo el tiempo de cache, revalidaciones, etc. Cuando usamos el hook useQuery
, este nos retorna un objeto con varias propiedades que nos permiten manejar el estado de la query, como por ejemplo data
, error
, isLoading
, etc.
Un ejemplo del uso completo que podemos hacer con una query es el siguiente, donde vamos a obtener una lista de productos en un panel administrativo:
import { useQuery } from '@tanstack/react-query'
function Products() {
const {
data,
error,
isPending,
isError,
isLoading,
isSuccess,
} = useQuery({
queryKey: ['products'],
queryFn: async () => {
const response = await fetch('/api/products')
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
},
staleTime: 1000 * 60 * 5, // 5 minutes -> Tiempo que los datos se consideran frescos antes de ser revalidados
cacheTime: 1000 * 60 * 10, // 10 minutes -> Tiempo que los datos se mantienen en cache después de que la query ya no está en uso
refetchOnWindowFocus: false, // No revalidar al enfocar la ventana
retry: 5, // Reintentar 5 veces en caso de error
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff para los reintentos
})
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error: {error.message}</div>
}
return (
<ul>
{data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
Query Keys
Las query keys son una parte fundamental de React Query, ya que nos permiten identificar de manera única cada query en nuestra aplicación. Una query key puede ser un array de valores donde es importante tener al menos un valor para identificar la query, pero podemos usar tantos elementos como necesitemos lo cual nos permite crear keys más complejas y específicas, además de que no solo podemos usar strings para identificar las queries, sino que podemos usar todo tipo de datos como números, arreglos, objetos, etc. Lo que necesitemos para identificar la query. Es importante que las query keys sean únicas para cada query, ya que React Query utiliza estas keys para almacenar y gestionar el estado de las queries en su cache. Por ejemplo, si tenemos una query para obtener los detalles de un producto específico, podemos usar un array como query key que incluya el ID del producto:
const { data } = useQuery({
queryKey: ['product', productId],
queryFn: async () => {
const response = await fetch(`/api/products/${productId}`)
return response.json()
},
})
Query Functions
Las query functions son las funciones que utilizamos para obtener los datos en nuestras queries, estas funciones deben retornar una promesa que resuelva con los datos que queremos obtener. Las query functions pueden ser funciones asíncronas o funciones que retornen una promesa de manera explícita. Es importante manejar los errores dentro de las query functions, ya que si la promesa es rechazada, React Query considerará que la query ha fallado y actualizará el estado de la query en consecuencia. Es importante, recalcar que las query functions su objetivo es regresar la data con la cual vamos a trabajar en React Query, sin importar de donde provenga, ya sea de una petición http, una petición graphql, del localStorage, etc. Además también, las query functions reciben un objeto como parámetro que contiene información útil sobre la query, como por ejemplo la query key, el page param en caso de que estemos trabajando con paginación, etc. Un ejemplo de una query function que maneja errores podría ser el siguiente:
const fetchProduct = async (productId) => {
const response = await fetch(`/api/products/${productId}`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
const { data } = useQuery({
queryKey: ['product', productId],
queryFn: ({ queryKey }) => fetchProduct(queryKey[1]),
})
Parallel Queries
Cuando nosotros usamos multiples queries en un mismo componente o hook de React, estas se ejecutan de manera paralela, es decir, todas las queries se inician al mismo tiempo y no esperan a que una termine para iniciar la siguiente, esto es muy útil cuando necesitamos obtener varios datos al mismo tiempo y no dependen unos de otros. Un ejemplo de uso de parallel queries podría ser el siguiente, donde obtenemos una lista de productos y una lista de categorías al mismo tiempo. Dentro de estos casos existe un caso en particular y es cuando necesitas hacer multiples queries, que usan la misma función y solo cambia por ejemplo un parámetro, supongamos que tenemos una función una función que obtiene los productos de una categoría específica, y queremos obtener los productos de varias categorías al mismo tiempo, en este caso podemos usar el hook useQueries
, que nos permite ejecutar multiples queries de manera paralela, pasando un array de objetos de configuración para cada query:
import { useQueries } from '@tanstack/react-query'
function ProductsByCategories({ categories }) {
const queries = useQueries({
queries: categories.map(({ id }) => ({
queryKey: ['products', id],
queryFn: async () => {
const response = await fetch(`/api/categories/${id}/products`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
},
})),
})
if (queries.some((query) => query.isLoading)) {
return <div>Loading...</div>
}
if (queries.some((query) => query.isError)) {
return <div>Error loading products</div>
}
return (
<div>
{queries.map((query, index) => (
<div key={categories[index]}>
<h2>Category {categories[index].name}</h2>
<ul>
{query.data.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
))}
</div>
)
}
Dependent Queries
Las dependent queries son queries que dependen del resultado de otra query para poder ejecutarse, es decir, una query no se ejecuta hasta que otra query haya terminado y haya proporcionado los datos necesarios. Esto es útil cuando necesitamos obtener datos que dependen de otros datos, por ejemplo, si queremos obtener los comentarios de un producto, después de haber obtenido los detalles del mismo. Para manejar dependent queries en React Query, podemos usar la opción enabled
en el hook useQuery
, esta opción nos permite controlar si una query debe ejecutarse o no, basándonos en alguna condición. Un ejemplo de uso de dependent queries podría ser el siguiente:
const { data: product } = useQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId),
})
const { data: reviews } = useQuery({
queryKey: ['reviews', productId],
queryFn: () => fetchReviews(productId),
enabled: !!product, // La query de reviews solo se ejecuta si product tiene un valor
})
Paginated Queries
Aunque la paginación en React Query es algo que se suele trabajar más con lo que son las Infinite Queries, también podemos manejar la paginación de una manera más tradicional usando queries normales, para esto podemos usar el hook useQuery
y manejar el estado de la página nosotros mismos, pasando el número de página como parte de la query key y usando este valor en la query function para obtener los datos correspondientes a esa página. Un ejemplo de uso de paginated queries podría ser el siguiente:
function PaginatedProducts() {
const [page, setPage] = useState(1)
const { data, isLoading, isError } = useQuery({
queryKey: ['products', page],
queryFn: ({ queryKey }) =>
fetch(`/api/products?page=${queryKey[1]}`).then((res) => res.json()),
keepPreviousData: true, // Mantener los datos anteriores mientras se carga la nueva página
})
if (isLoading) {
return <div>Loading...</div>
}
if (isError) {
return <div>Error loading products</div>
}
return (
<div>
<ul>
{data.products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<button
onClick={() => setPage((old) => Math.max(old - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<button
onClick={() => setPage((old) => (!data.hasMore ? old : old + 1))}
disabled={!data.hasMore}
>
Next
</button>
</div>
)
}
Infinite Queries
Las infinite queries son una forma de manejar la paginación de datos de manera más dinámica, permitiéndonos cargar más datos a medida que el usuario los necesita, en lugar de cargar todos los datos de una sola vez o tener que navegar entre páginas. Para manejar infinite queries en React Query, podemos usar el hook useInfiniteQuery
, este hook nos permite definir una query function que recibe un parámetro especial llamado pageParam
, el cual podemos usar para obtener la siguiente página de datos. Además, el hook useInfiniteQuery
nos proporciona un método llamado fetchNextPage
, que podemos usar para cargar la siguiente página de datos cuando sea necesario. Un ejemplo de uso de infinite queries podría ser el siguiente:
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteProducts() {
const {
data,
isLoading,
isError,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['products'],
queryFn: ({ pageParam = 1 }) => fetch(`/api/products?page=${pageParam}`).then((res) => res.json()),
initialPageParam: 1,
getNextPageParam: (lastPage, pages) => {
if (lastPage.hasMore) {
return pages.length + 1 // Siguiente página
}
return undefined // No hay más páginas
},
})
if (isLoading) {
return <div>Loading...</div>
}
if (isError) {
return <div>Error loading products</div>
}
return (
<div>
{data.pages.map((page, pageIndex) => (
<div key={pageIndex}>
<ul>
{page.products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'No more products'}
</button>
</div>
)
}
Mutations
Las mutaciones a diferencia de las queries, no son para obtener datos, sino para modificarlos, ya sea creando, actualizando o eliminando datos. Para manejar mutaciones en React Query, podemos usar el hook useMutation
, este hook nos permite definir una función que realiza la mutación y nos proporciona métodos para ejecutar la mutación y manejar su estado. Un ejemplo de uso de mutaciones podría ser el siguiente, donde creamos un nuevo producto:
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreateProduct() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationKey: ['createProduct'],
mutationFn: async (newProduct) => {
// Función para crear un nuevo producto
await addProduct(newProduct)
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['products'] })
},
onError: (error) => {
console.error('Error creating product:', error)
},
onSettled: () => {
console.log('Mutation settled')
},
})
const handleSubmit = (event) => {
event.preventDefault()
const form = new FormData(event.target)
const name = form.get('name') as string
const price = parseFloat(form.get('price') as string)
const newProduct = { name, price }
mutation.mutate(newProduct)
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Product Name" required />
<input name="price" type="number" step="0.01" placeholder="Price" required />
<button type="submit">Create Product</button>
{mutation.isLoading && <p>Creating product...</p>}
{mutation.isError && <p>Error creating product</p>}
{mutation.isSuccess && <p>Product created!</p>}
</form>
)
}
Query Invalidation
Como lo vimos en el ejemplo anterior, una de las cosas más comunes que hacemos después de una mutación es invalidar las queries relacionadas para asegurarnos de que los datos en la UI estén actualizados. React Query nos proporciona el método invalidateQueries
a través del QueryClient
, que podemos usar en los callbacks de la mutación como onSuccess
, onError
o onSettled
. Al invalidar una query, React Query marcará esa query como desactualizada y la volverá a ejecutar la próxima vez que se necesite, obteniendo así los datos más recientes. Esto es especialmente útil cuando una mutación afecta a los datos que ya hemos obtenido con una query, como en el caso de crear, actualizar o eliminar un recurso.
Mutation Responses
Si queremos actualizar la data de algún recurso una vez que hemos echo una mutación no solo podemos hacerlo invalidando las queries relacionadas, sino que también podemos usar la respuesta de la mutación para actualizar directamente la cache de React Query. Esto es útil cuando la respuesta de la mutación contiene los datos actualizados que queremos reflejar en la UI sin necesidad de hacer una nueva petición para obtenerlos. Para hacer esto, podemos usar el método setQueryData
del QueryClient
, que nos permite actualizar los datos almacenados en la cache para una query específica. Un ejemplo de esto podría ser el siguiente, donde actualizamos un producto y usamos la respuesta de la mutación para actualizar directamente la cache:
const mutation = useMutation({
mutationKey: ['updateProduct'],
mutationFn: async (updatedProduct) => {
// Función para actualizar un producto
return await updateProduct(updatedProduct)
},
onSuccess: (data) => {
// Actualizar la cache directamente con la respuesta de la mutación
queryClient.setQueryData(['product', data.id], data)
queryClient.invalidateQueries({ queryKey: ['products'] }) // Opcionalmente invalidar la lista de productos
},
})
Optimistic Updates
Las actualizaciones optimistas son una técnica que nos permite actualizar la UI inmediatamente después de iniciar una mutación, sin esperar a que la mutación se complete. Esto mejora la experiencia del usuario al hacer que la aplicación se sienta más rápida y responsiva. Para implementar actualizaciones optimistas en React Query, podemos usar el callback onMutate
del hook useMutation
. En este callback, podemos actualizar la cache de React Query para reflejar los cambios de manera inmediata. Además, es importante manejar el caso en que la mutación falle, para lo cual podemos usar el callback onError
para revertir los cambios si es necesario. Un ejemplo de uso de actualizaciones optimistas podría ser el siguiente:
const mutation = useMutation({
mutationKey: ['updateProduct'],
mutationFn: async (updatedProduct) => {
// Función para actualizar un producto
return await updateProduct(updatedProduct)
},
onMutate: async (newProduct) => {
// Cancelar cualquier query en curso para el producto
await queryClient.cancelQueries({ queryKey: ['product', newProduct.id] })
// Obtener el estado anterior del producto
const previousProduct = queryClient.getQueryData(['product', newProduct.id])
// Actualizar la cache de manera optimista
queryClient.setQueryData(['product', newProduct.id], (old) => ({
...old,
...newProduct,
}))
// Retornar el estado anterior para poder revertir lo en caso de error
return { previousProduct }
},
onError: (err, newProduct, context) => {
// Revertir los cambios en caso de error
if (context?.previousProduct) {
queryClient.setQueryData(['product', newProduct.id], context.previousProduct)
}
},
onSettled: () => {
// Invalidar la query para asegurarnos de que los datos estén actualizados
queryClient.invalidateQueries({ queryKey: ['products'] })
},
})
Caching y temas avanzados
Cuando trabajamos con React Query, el manejo del cache es uno de los aspectos más importantes y poderosos de la librería. React Query almacena en cache los datos obtenidos por las queries, lo que nos permite evitar peticiones innecesarias y mejorar el rendimiento de nuestra aplicación. Podemos configurar el comportamiento del cache a través de varias opciones disponibles en el hook useQuery
, como staleTime
, cacheTime
, refetchOnWindowFocus
, entre otras.
Prefetching Queries
El prefetching de queries es una técnica que nos permite cargar datos en cache antes de que sean necesarios en la UI, lo que puede mejorar significativamente la experiencia del usuario al reducir los tiempos de espera. React Query nos proporciona el método prefetchQuery
a través del QueryClient
, que podemos usar para cargar datos en cache de manera anticipada. Un ejemplo de uso de prefetching podría ser el siguiente, donde pre cargamos los detalles de un producto cuando el usuario pasa el cursor sobre un enlace:
import { useQueryClient } from '@tanstack/react-query'
function ProductLink({ productId, children }) {
const queryClient = useQueryClient()
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['product', productId],
queryFn: () => fetchProduct(productId),
})
}
return (
<a href={`/products/${productId}`} onMouseEnter={handleMouseEnter}>
{children}
</a>
)
}