Si has construido una aplicación React de cierta complejidad, seguramente has escrito el mismo boilerplate decenas de veces: un useEffect para obtener datos, useState para loading y error, invalidación manual del caché cuando algo cambia. React Query reemplaza todo eso con una API pequeña y bien diseñada que maneja el caché, el refetching en segundo plano y la gestión de datos desactualizados out of the box.

¿Qué es React Query?

React Query es una biblioteca para obtener, almacenar en caché y actualizar el estado del servidor en aplicaciones React. No reemplaza a Redux o Zustand para el estado del lado del cliente; maneja una categoría de estado completamente diferente. El estado del servidor reside en un sistema remoto, puede volverse obsoleto y necesita sincronización. React Query fue construido específicamente para ese problema.

Al manejar el caché y las actualizaciones en segundo plano internamente, reduce la cantidad de estado que gestionas manualmente y hace que tus componentes sean más fáciles de razonar.

¿Por Qué Usar React Query?

Gestionar la obtención de datos por tu cuenta implica rastrear al menos tres partes del estado por solicitud: loading, error y data. Agrega paginación, refetch al enfocar la ventana, deduplicación de solicitudes concurrentes y lógica de reintentos, y tendrás cientos de líneas de infraestructura para un problema que no es tu producto. React Query resuelve todo esto a nivel de biblioteca.

Ventajas específicas:

  • Cuando múltiples componentes solicitan los mismos datos simultáneamente, React Query deduplica la solicitud de red y comparte el resultado.
  • Los datos se obtienen automáticamente en segundo plano cuando se vuelven obsoletos, por lo que los usuarios ven contenido actualizado sin una recarga manual.
  • Las actualizaciones optimistas y la invalidación del caché mantienen la UI consistente tras las mutaciones sin código de sincronización complejo.

Comenzando con React Query

Instalación:

npm install @tanstack/react-query

Antes de usar los hooks de React Query, configura un QueryClient y envuelve tu aplicación con un QueryClientProvider:

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* ... tus componentes */}
    </QueryClientProvider>
  );
}

export default App;

Entendiendo Queries y Mutations

React Query proporciona dos hooks principales: useQuery para obtener datos y useMutation para crear, actualizar o eliminar datos.

El Hook useQuery:

import { useQuery } from '@tanstack/react-query';

function Todos() {
  const { isLoading, error, data } = useQuery(['todos'], fetchTodos);

  if (isLoading) return 'Loading...';

  if (error) return 'An error occurred';

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}

El Hook useMutation:

import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddTodo() {
  const queryClient = useQueryClient();
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries(['todos']);
    },
  });

  return (
    <button
      onClick={() => {
        mutation.mutate({ title: 'New Todo' });
      }}
    >
      Add Todo
    </button>
  );
}

Configurando un Caso de Uso Completo

Aquí hay una aplicación CRUD básica para gestionar una lista de usuarios, usando la API JSONPlaceholder.

La aplicación maneja: mostrar una lista de usuarios, agregar un nuevo usuario, actualizar un usuario existente y eliminar un usuario.

Configurando el Proyecto:

npx create-react-app react-query-demo
cd react-query-demo
npm install @tanstack/react-query axios

Usaremos Axios para hacer las solicitudes HTTP.

Implementando React Query en la Aplicación:

  1. Configurar QueryClient
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);
  1. Obtener Usuarios
// App.js
import React from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';

function App() {
  const { isLoading, error, data } = useQuery(['users'], () =>
    axios.get('https://jsonplaceholder.typicode.com/users').then(res => res.data)
  );

  if (isLoading) return <p>Loading users...</p>;

  if (error) return <p>An error occurred: {error.message}</p>;

  return (
    <div>
      <h1>User List</h1>
      <ul>
        {data.map(user => (
          <li key={user.id}>
            {user.name}
            {/* Update and Delete buttons will go here */}
          </li>
        ))}
      </ul>
      {/* Components for adding a user will go here */}
    </div>
  );
}

export default App;
  1. Agregar un Usuario

Como JSONPlaceholder no cambia los datos realmente, simularemos la adición de un usuario.

// AddUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddUser() {
  const queryClient = useQueryClient();
  const [name, setName] = useState('');

  const addUserMutation = useMutation(
    newUser => axios.post('https://jsonplaceholder.typicode.com/users', newUser).then(res => res.data),
    {
      onSuccess: data => {
        // Update the users list
        queryClient.setQueryData(['users'], oldData => [...oldData, data]);
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    addUserMutation.mutate({ name });
    setName('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" required />
      <button type="submit">Add User</button>
    </form>
  );
}

export default AddUser;

Incluye <AddUser /> en tu componente App.

// App.js
// ...
import AddUser from './AddUser';
// ...
function App() {
  // existing code
  return (
    <div>
      {/* existing code */}
      <AddUser />
    </div>
  );
}
// ...
  1. Actualizar un Usuario
// UpdateUser.js
import React, { useState } from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function UpdateUser({ user }) {
  const queryClient = useQueryClient();
  const [name, setName] = useState(user.name);

  const updateUserMutation = useMutation(
    updatedUser => axios.put(`https://jsonplaceholder.typicode.com/users/${user.id}`, updatedUser).then(res => res.data),
    {
      onSuccess: data => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.map(u => (u.id === user.id ? data : u))
        );
      },
    }
  );

  const handleSubmit = e => {
    e.preventDefault();
    updateUserMutation.mutate({ name });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={e => setName(e.target.value)} required />
      <button type="submit">Update</button>
    </form>
  );
}

export default UpdateUser;

Incluye <UpdateUser user={user} /> en el renderizado de tu lista de usuarios.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  {/* Delete button */}
</li>
// ...
  1. Eliminar un Usuario
// DeleteUser.js
import React from 'react';
import axios from 'axios';
import { useMutation, useQueryClient } from '@tanstack/react-query';

function DeleteUser({ userId }) {
  const queryClient = useQueryClient();

  const deleteUserMutation = useMutation(
    () => axios.delete(`https://jsonplaceholder.typicode.com/users/${userId}`),
    {
      onSuccess: () => {
        queryClient.setQueryData(['users'], oldData =>
          oldData.filter(user => user.id !== userId)
        );
      },
    }
  );

  return <button onClick={() => deleteUserMutation.mutate()}>Delete</button>;
}

export default DeleteUser;

Incluye <DeleteUser userId={user.id} /> en el renderizado de tu lista de usuarios.

// App.js
// ...
<li key={user.id}>
  {user.name}
  <UpdateUser user={user} />
  <DeleteUser userId={user.id} />
</li>
// ...

Funcionalidades Avanzadas

Manejando Estados de Query:

React Query proporciona varios estados para gestionar escenarios de loading, error y éxito de manera efectiva.

const {
  isLoading,
  isError,
  data,
  error,
  isFetching,
  refetch,
} = useQuery(['todos'], fetchTodos);
  • isLoading: true en la primera carga antes de que lleguen los datos.
  • isError: true si la query encuentra un error.
  • data: el resultado obtenido.
  • error: el objeto de error cuando la query falla.
  • isFetching: permanece true durante los refetches en segundo plano incluso cuando los datos ya están en caché.
  • refetch: llama a esto para disparar un refetch manualmente.

Estrategias de Caché:

useQuery(['users'], fetchUsers, {
  cacheTime: 1000 * 60 * 10, // Cache data for 10 minutes
  staleTime: 1000 * 60 * 5,  // Data is fresh for 5 minutes
});

Polling o Obtención por Intervalo:

useQuery(['notifications'], fetchNotifications, {
  refetchInterval: 1000 * 60, // Refetch every 60 seconds
});

Reintentando Queries Fallidas:

useQuery(['users'], fetchUsers, {
  retry: 3, // Retry up to 3 times
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
});

React Query Devtools:

npm install @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    // ...
    <ReactQueryDevtools initialIsOpen={false} />
  );
}

Server-Side Rendering:

// pages/_app.js
// Implement getServerSideProps or getStaticProps

Funciones de Query Personalizadas:

const fetcher = url => axios.get(url).then(res => res.data);

useQuery(['users'], () => fetcher('/api/users'));

Transformando Datos:

useQuery(['users'], fetchUsers, {
  select: data => data.map(user => user.name),
});

Buenas Prácticas

  • Usa query keys descriptivas y estructuradas: ['users', userId] es más fácil de invalidar selectivamente que 'user'.
  • Maneja los errores con callbacks onError y muéstralos explícitamente al usuario.
  • Usa paginación o scroll infinito para grandes conjuntos de datos para evitar cargar todo de una vez.
  • Ajusta el staleTime por query. Los datos que cambian raramente pueden tener un staleTime alto; los datos en tiempo real deben tener cero.

Alternativas a React Query

SWR, desarrollado por Vercel, cubre territorio similar con una API más simple pero con menos funcionalidades avanzadas. Apollo Client vale la pena considerarlo si ya usas GraphQL y quieres normalización de caché integrada.

Conclusión

React Query elimina una categoría sustancial de boilerplate de las aplicaciones React. Después de integrarlo, notarás que la mayor parte de tu lógica de data fetching con useEffect desaparece, y los componentes que quedan son más fáciles de testear y razonar. La curva de aprendizaje está en el sistema de query keys y en entender cuándo invalidar versus actualizar optimistamente, pero ambos se vuelven intuitivos rápidamente.

Lectura Adicional