Interstellar Code
Guía de Redux Toolkit Query
Redux Toolkit Query (RTK Query) es una poderosa herramienta para la gestión de datos asíncronos en aplicaciones React, es una librería muy parecida o con un enfoque bastante similar a lo que ofrece TanStack React Query, pero con una forma de integrarla con Redux Toolkit de una forma muy sencilla. Esta guía te enseñará cómo utilizar RTK Query para simplificar la obtención, almacenamiento en caché y sincronización de datos en tu aplicación, mejorando el rendimiento y la experiencia del usuario.
29 de agosto de 2025
Tabla de contenido:
Guía de Redux Toolkit Query
Redux Toolkit Query (RTK Query) es una poderosa herramienta para la gestión de datos asíncronos en aplicaciones React. Proporciona una forma sencilla y eficiente de manejar la obtención, almacenamiento en caché y sincronización de datos, mejorando el rendimiento y la experiencia del usuario.
Esta herramienta está incluida en la librería de Redux Toolkit, lo que facilita su integración con Redux y permite aprovechar las ventajas de ambas tecnologías, permitiendo gestionar el estado global de la aplicación y los datos asíncronos de manera coherente y eficiente.
Para poder trabajar con esta herramienta lo primero y más importante es tener instaladas las dependencias necesarias en nuestro proyecto, para ello debemos de instalar @reduxjs/toolkit
y react-redux
si no las tenemos ya instaladas:
npm install @reduxjs/toolkit react-redux
Configuración de RTK Query
Para poder utilizar RTK Query en nuestra aplicación, lo primero que debemos hacer es configurar un “API Slice”. Un API Slice es una pieza de código que define cómo interactuar con una API específica, incluyendo las consultas (queries) y mutaciones (mutations) que se pueden realizar.
Para crear un API Slice, podemos utilizar la función createApi
proporcionada por RTK Query. Aquí hay un ejemplo básico de cómo configurar un API Slice:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { Post } from './types';
export const apiPosts = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3000/'
}),
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => 'posts',
}),
getPostById: builder.query<Post, number>({
query: (id) => `posts/${id}`,
}),
createPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({
url: 'posts',
method: 'POST',
body,
}),
}),
updatePost: builder.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
query: ({ id, ...body }) => ({
url: `posts/${id}`,
method: 'PUT',
body,
}),
}),
deletePost: builder.mutation<{ success: boolean; id: number }, number>({
query: (id) => ({
url: `posts/${id}`,
method: 'DELETE',
}),
}),
}),
});
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = apiPosts;
Una vez que tenemos nuestra API Slice configurada, debemos integrarla en nuestro store de Redux. Para ello, necesitamos añadir el reducer del API Slice y el middleware proporcionado por RTK Query:
import { configureStore } from '@reduxjs/toolkit';
import { apiPosts } from './apiPosts';
export const store = configureStore({
reducer: {
[apiPosts.reducerPath]: apiPosts.reducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware()
.concat(apiPosts.middleware),
});
Por último, debemos envolver nuestra aplicación con el proveedor de Redux para que los componentes puedan acceder al store:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store'
import { App } from './App'
const rootItem = document.getElementById('root')
const root = createRoot(rootItem)
root.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);
Uso Standard de RTK Query
El uso estándar de RTK Query se basa en dos conceptos principales, los cuales son las Queries
y las Mutations
, las cuales nos ayudan a definir operaciones para lectura y escritura de datos respectivamente. Esto lo podemos hacer empleando cualquier herramienta que nos permita hacer peticiones de datos, ya sean consultas HTTP Rest, GraphQL, tRPC, etc.
Queries
Para realizar consultas de datos, RTK Query proporciona hooks generados automáticamente basados en los endpoints definidos en el API Slice. Estos hooks se utilizan dentro de los componentes React para obtener datos y manejar estados de carga y error. Aquí hay un ejemplo de cómo usar un hook de consulta en un componente:
import React from 'react';
import { useGetPostsQuery } from './apiPosts';
import type { Post } from './types';
import { PostCard } from './PostCard';
export const PostsList: React.FC = () => {
const { data: posts, error, isLoading } = useGetPostsQuery(undefined, {
pollingInterval: 60000, // Refetch data every 60 seconds
refetchOnFocus: true, // Refetch data when window is focused
refetchOnReconnect: true // Refetch data when network reconnects
refetchOnMountOrArgChange: true, // Refetch data when component mounts or argument changes
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading posts</div>;
return (
<div>
{posts?.map((post: Post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
};
Los hooks generados por RTK Query proporcionan varios estados útiles, como isLoading
, isSuccess
, isError
, y data
, que facilitan la gestión del ciclo de vida de las solicitudes de datos. Pero en total los datos que nos regresan estos hooks son:
- data: Contiene los datos obtenidos de la consulta si la solicitud fue exitosa.
- currentData: Contiene los datos actuales en caché, incluso si la solicitud está en curso.
- error: Contiene información sobre cualquier error que haya ocurrido durante la solicitud.
- isUninitialized: Indica si la consulta aún no se ha iniciado.
- isLoading: Indica si la consulta está en curso.
- isSuccess: Indica si la consulta se completó con éxito.
- isError: Indica si la consulta falló.
- isFetching: Indica si la consulta está en proceso de obtención de datos
- refetch: Función para volver a ejecutar la consulta manualmente.
Infinite Queries
Las infinite queries son un patrón comúnmente utilizado para implementar la paginación en aplicaciones web. RTK Query no tiene soporte nativo para infinite queries como lo hace React Query, pero podemos implementar este patrón utilizando consultas normales y gestionando el estado de la paginación manualmente. Aquí hay un ejemplo básico de cómo podríamos implementar infinite queries con RTK Query:
Primero definimos un endpoint en nuestro API Slice que acepte parámetros de paginación:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { Post } from './types';
export const apiPosts = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3000/'
}),
endpoints: (builder) => ({
getInfinitePosts: builder.infiniteQuery<Post[], string, number>({
infiniteQueryOptions: {
initialPageParam: 1,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams, queryArg) => {
return lastPageParam + 1;
},
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams, queryArg) => {
return firstPageParam - 1 > 0 ? firstPageParam - 1 : undefined;
},
},
query({ queryArg, pageParam }) {
return `posts/${queryArg}?page=${pageParam}`;
}
}),
}),
});
Luego, en nuestro componente, podemos utilizar el hook generado para manejar la paginación:
import React from 'react';
import { useGetInfinitePostsQuery } from './apiPosts';
import type { Post } from './types';
import { PostCard } from './PostCard';
export const InfinitePostsList: React.FC = () => {
const {
data,
error,
isLoading,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useGetInfinitePostsQuery('all', {
initialPageParam: 1,
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading posts</div>;
return (
<div>
{data?.pages.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{page.map((post: Post) => (
<PostCard key={post.id} post={post} />
))}
</React.Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'No more posts'}
</button>
</div>
);
};
El hook useGetInfinitePostsQuery
nos proporciona varios estados útiles para manejar la paginación, como isFetchingNextPage
, fetchNextPage
, y hasNextPage
. Esto nos permite cargar más datos cuando el usuario lo solicite, implementando así el patrón de infinite scrolling o paginación. En general la información que nos regresa este hook es:
- data: Contiene los datos obtenidos de la consulta si la solicitud fue exitosa, organizados en páginas.
- currentData: Contiene los datos actuales en caché, incluso si la solicitud está en curso.
- hasNextPage: Indica si hay más páginas disponibles para cargar.
- hasPreviousPage: Indica si hay páginas anteriores disponibles para cargar.
- isFetchingNextPage: Indica si se está cargando la siguiente página.
- isFetchingPreviousPage: Indica si se está cargando la página anterior.
- isFetchNextPageError: Indica si hubo un error al intentar cargar la siguiente página.
- isFetchPreviousPageError: Indica si hubo un error al intentar cargar la
- fetchNextPage: Función para cargar la siguiente página de datos.
- fetchPreviousPage: Función para cargar la página anterior de datos.
Mutations
Las mutaciones como su nombre lo indicia son operaciones que modifican datos en el servidor, como crear, actualizar o eliminar recursos. Al igual que con las consultas, RTK Query genera automáticamente hooks para las mutaciones basadas en los endpoints definidos en el API Slice. Aquí hay un ejemplo de cómo usar un hook de mutación en un componente:
import React, { useState } from 'react';
import { useCreatePostMutation } from './apiPosts';
import type { Post } from './types';
import { PostForm } from './PostForm';
export const CreatePost: React.FC = () => {
const [createPost, { isLoading, isSuccess, isError, error }] = useCreatePostMutation();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await createPost({ title, content });
setTitle('');
setContent('');
};
return (
<div>
<h2>Create New Post</h2>
<PostForm
title={title}
content={content}
onTitleChange={(e) => setTitle(e.target.value)}
onContentChange={(e) => setContent(e.target.value)}
onSubmit={handleSubmit}
isLoading={isLoading}
/>
{isSuccess && <div>Post created successfully!</div>}
{isError && <div>Error creating post: {error?.message}</div>}
</div>
);
};
Los hooks de mutación proporcionan estados similares a los de las consultas, como isLoading
, isSuccess
, isError
, y data
, que facilitan la gestión del ciclo de vida de las solicitudes de mutación. Pero en total los datos que nos regresan estos hooks son:
- data: Contiene los datos devueltos por la mutación si la solicitud fue exitosa.
- error: Contiene información sobre cualquier error que haya ocurrido durante la solicitud.
- isUninitialized: Indica si la mutación aún no se ha iniciado.
- isLoading: Indica si la mutación está en curso.
- isSuccess: Indica si la mutación se completó con éxito.
- isError: Indica si la mutación falló.
Invalidación de caché
Es muy común que después de realizar una mutación, los datos en caché puedan quedar desactualizados. RTK Query proporciona un mecanismo para invalidar la caché y forzar la actualización de los datos relacionados. Para esto debemos de manejar la revalidación de la información en cache utilizando la propiedad providesTags
en las queries y invalidatesTags
en las mutations. Aquí hay un ejemplo de cómo hacerlo:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import type { Post } from './types';
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3000/'
}),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => 'posts',
// Los provideTags nos permiten definir qué datos en caché están relacionados con esta consulta, y por lo tanto, qué datos deben ser invalidados cuando se realice una mutación que afecte a estos datos.
providesTags: (result) => {
if (result) {
return [
...result.map(({ id }) => ({ type: 'Posts', id }) as const),
{ type: 'Posts', id: 'LIST' },
];
} else {
return [{ type: 'Posts', id: 'LIST' }];
}
}
}),
addPost: build.mutation<Post, Partial<Post>>({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
// Invalidamos la caché de la lista de posts para que se vuelva a obtener la lista actualizada después de agregar un nuevo post.
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
// Definimos que esta consulta proporciona un tag específico para el post con el id dado.
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
updatePost: build.mutation<Post, Partial<Post>>({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
// Invalidamos la caché del post específico que se ha actualizado.
invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidamos la caché del post específico que se ha eliminado.
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
export const {
useGetPostsQuery,
useAddPostMutation,
useGetPostQuery,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi