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:

  1. 1. Guía de Redux Toolkit Query
  2. 2. Configuración de RTK Query
  3. 3. Uso Standard de RTK Query
  4. 4. Queries
  5. 5. Infinite Queries
  6. 6. Mutations
  7. 7. Invalidación de caché

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:

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:

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:

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