Back

Uso de TanStack Query para una Obtención de Datos más Inteligente en React

Uso de TanStack Query para una Obtención de Datos más Inteligente en React

Si has desarrollado aplicaciones React que se comunican con APIs, conoces el patrón: useEffect para la petición, useState para los datos, otro useState para la carga, tal vez uno más para los errores. Antes de darte cuenta, estás gestionando un enredo de estado solo para mostrar una lista de usuarios.

Este enfoque manual funciona, pero es frágil. ¿Qué pasa cuando el usuario navega fuera de la página y regresa? ¿Vuelves a hacer la petición? ¿Usas datos obsoletos? ¿Qué hay de los reintentos cuando falla la red? Estos no son casos extremos—son la realidad de las aplicaciones en producción.

TanStack Query (anteriormente React Query) resuelve estos problemas tratando el estado del servidor de manera diferente al estado del cliente. En lugar de obtener datos de forma imperativa, declaras lo que necesitas y dejas que la librería maneje el caché, la sincronización y las actualizaciones. Este artículo te muestra cómo pasar de la obtención manual de datos a un enfoque declarativo más inteligente que escala.

Puntos Clave

  • Reemplaza los patrones manuales de useEffect + useState con hooks declarativos useQuery para un código más limpio y mantenible
  • Aprovecha el caché automático, la recarga en segundo plano y la deduplicación de peticiones para mejorar la experiencia del usuario
  • Usa mutaciones con actualizaciones optimistas para crear interfaces de usuario responsivas que se sienten instantáneas
  • Implementa estrategias adecuadas de invalidación de consultas para mantener los datos sincronizados en toda tu aplicación
  • Evita errores comunes como usar TanStack Query para estado local u olvidar parámetros dinámicos en las claves de consulta

El Problema con la Obtención Manual de Datos

Aquí tienes un componente React típico que obtiene datos:

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    
    async function fetchUsers() {
      setIsLoading(true);
      setError(null);
      
      try {
        const response = await fetch('/api/users');
        if (!response.ok) throw new Error('Failed to fetch');
        
        const data = await response.json();
        if (!cancelled) {
          setUsers(data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    }
    
    fetchUsers();
    
    return () => { cancelled = true };
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Esto funciona, pero observa lo que estamos gestionando:

  • Tres piezas separadas de estado
  • Lógica de limpieza para prevenir actualizaciones de estado en componentes desmontados
  • Sin caché—cada montaje dispara una nueva petición
  • Sin lógica de reintento para peticiones fallidas
  • Sin manera de saber si nuestros datos están obsoletos

Ahora multiplica esta complejidad por docenas de componentes. Ahí es donde entra TanStack Query.

Cómo TanStack Query Hace la Obtención de Datos Más Inteligente

TanStack Query trata el estado del servidor como un ciudadano de primera clase. En lugar de orquestar manualmente las peticiones, describes tus requisitos de datos de forma declarativa:

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

function UserList() {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(res => {
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    }),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

La misma funcionalidad, pero observa lo que falta:

  • No hay useState para datos, carga o errores
  • No hay useEffect o lógica de limpieza
  • No hay sincronización manual de estado

Pero esto es lo que obtienes:

  • Caché automático: Navega fuera y regresa, ve resultados instantáneos
  • Recarga en segundo plano: Los datos obsoletos se actualizan silenciosamente
  • Deduplicación de peticiones: Múltiples componentes pueden compartir la misma consulta
  • Lógica de reintento incorporada: Las peticiones fallidas se reintentan automáticamente
  • Actualizaciones optimistas: La UI se actualiza antes de la confirmación del servidor

Conceptos Fundamentales para una Gestión de Datos Más Inteligente

Consultas: Leyendo el Estado del Servidor

Las consultas obtienen y cachean datos. El hook useQuery acepta un objeto de configuración con dos propiedades esenciales:

const { data, isLoading, error } = useQuery({
  queryKey: ['todos', userId, { status: 'active' }],
  queryFn: () => fetchTodosByUser(userId, { status: 'active' }),
  staleTime: 5 * 60 * 1000, // 5 minutos
  gcTime: 10 * 60 * 1000, // 10 minutos (anteriormente cacheTime)
});

Las Claves de Consulta identifican únicamente cada consulta. Son arrays que pueden incluir:

  • Identificadores estáticos: ['users']
  • Parámetros dinámicos: ['user', userId]
  • Filtros complejos: ['todos', { status, page }]

Cuando cualquier parte de la clave de consulta cambia, TanStack Query sabe que debe obtener datos frescos.

Mutaciones: Cambiando el Estado del Servidor

Mientras las consultas leen datos, las mutaciones los modifican:

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

function CreateTodo() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newTodo) => 
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
        headers: { 'Content-Type': 'application/json' },
      }).then(res => {
        if (!res.ok) throw new Error('Failed to create todo');
        return res.json();
      }),
    onSuccess: () => {
      // Invalidar y volver a obtener
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <button 
      onClick={() => mutation.mutate({ title: 'New Todo' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create Todo'}
    </button>
  );
}

Las mutaciones manejan el ciclo de vida completo: estados de carga, manejo de errores y callbacks de éxito. El callback onSuccess es perfecto para actualizar tu caché después de los cambios.

Invalidación de Consultas: Manteniendo los Datos Frescos

La invalidación marca las consultas como obsoletas, disparando recargas en segundo plano:

// Invalidar todo
queryClient.invalidateQueries();

// Invalidar consultas específicas
queryClient.invalidateQueries({ queryKey: ['todos'] });

// Invalidar con coincidencia exacta
queryClient.invalidateQueries({ 
  queryKey: ['todo', 5], 
  exact: true 
});

Así es como TanStack Query mantiene tu UI sincronizada después de las mutaciones. ¿Cambias un todo? Invalida la lista de todos. ¿Actualizas un usuario? Invalida los datos de ese usuario.

Configurando TanStack Query en tu Aplicación React

Primero, instala el paquete:

npm install @tanstack/react-query

Luego envuelve tu aplicación con el proveedor QueryClient:

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minuto
      gcTime: 5 * 60 * 1000, // 5 minutos (anteriormente cacheTime)
      retry: 3,
      retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

El QueryClient gestiona todo el caché y la sincronización. Típicamente creas una instancia para toda tu aplicación.

Patrones Avanzados para Aplicaciones en Producción

Actualizaciones Optimistas

Actualiza la UI inmediatamente, luego sincroniza con el servidor:

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // Cancelar recargas salientes
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    
    // Capturar valor anterior
    const previousTodos = queryClient.getQueryData(['todos']);
    
    // Actualizar optimistamente
    queryClient.setQueryData(['todos'], old => 
      old ? [...old, newTodo] : [newTodo]
    );
    
    // Devolver contexto para rollback
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Rollback en caso de error
    if (context?.previousTodos) {
      queryClient.setQueryData(['todos'], context.previousTodos);
    }
  },
  onSettled: () => {
    // Siempre recargar después de error o éxito
    queryClient.invalidateQueries({ queryKey: ['todos'] });
  },
});

Consultas Dependientes

Encadena consultas que dependen unas de otras:

const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: () => getUserByEmail(email),
});

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: () => getProjectsByUser(user.id),
  enabled: !!user?.id, // Solo ejecutar cuando existe el ID del usuario
});

Consultas Infinitas

Maneja datos paginados con scroll infinito:

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

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
} = useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: ({ pageParam = 0 }) => fetchProjects({ page: pageParam }),
  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  initialPageParam: 0,
});

Errores Comunes y Soluciones

Olvidar el QueryClientProvider

Si tus consultas devuelven undefined o no funcionan en absoluto, verifica que tu aplicación esté envuelta con QueryClientProvider. Este es el error de configuración más común.

Usar TanStack Query para Estado Local

TanStack Query es para estado del servidor. No lo uses para inputs de formularios, toggles de UI u otro estado exclusivo del cliente. Mantente con useState o useReducer para esos casos.

Claves de Consulta Estáticas con Datos Dinámicos

Siempre incluye parámetros dinámicos en tus claves de consulta:

// ❌ Incorrecto - mostrará los mismos datos para todos los usuarios
useQuery({
  queryKey: ['user'],
  queryFn: () => fetchUser(userId),
});

// ✅ Correcto - entrada de caché única por usuario
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

Conclusión

TanStack Query transforma la obtención de datos en React de un proceso manual y propenso a errores a un sistema declarativo y robusto. Al tratar el estado del servidor como fundamentalmente diferente del estado del cliente, elimina el código repetitivo mientras añade características poderosas como caché, sincronización y actualizaciones optimistas.

El cambio mental es simple: deja de pensar en cómo obtener datos y empieza a pensar en qué datos necesitas. Declara tus requisitos a través de claves y funciones de consulta, luego deja que TanStack Query maneje la complejidad de mantener todo sincronizado.

Empieza pequeño—reemplaza una petición con useEffect por useQuery. Una vez que veas los estados de carga instantáneos, los reintentos automáticos y el caché fluido en acción, entenderás por qué TanStack Query se ha vuelto esencial para las aplicaciones React modernas.

Preguntas Frecuentes

Son la misma librería. React Query fue renombrado a TanStack Query para reflejar que ahora soporta múltiples frameworks más allá de React. El paquete específico para React sigue siendo mantenido y usa la misma API con la que estás familiarizado.

Sí, pero probablemente no lo necesites para estado del servidor. TanStack Query maneja el caché y compartir datos entre componentes automáticamente. Usa Redux o Context para verdadero estado del cliente como preferencias de usuario o estado de UI, y TanStack Query para cualquier cosa que venga de una API.

Por defecto, las consultas se recargan al enfocar la ventana, reconectar y montar. Controlas la frescura con staleTime (cuánto tiempo permanecen frescos los datos) y gcTime (cuánto tiempo mantener datos no utilizados). Las consultas obsoletas se recargan en segundo plano mientras muestran datos cacheados instantáneamente.

La invalidación marca las consultas como obsoletas pero no recarga inmediatamente—espera a que la consulta sea usada de nuevo. La recarga directa dispara una petición de red inmediata. Usa invalidación después de mutaciones para mejor rendimiento, ya que solo recarga consultas que realmente se están mostrando.

TanStack Query sobresale en actualizaciones casi en tiempo real a través de polling e intervalos de recarga. Para necesidades verdaderamente en tiempo real, combínalo con WebSockets: usa el socket para actualizaciones en vivo y TanStack Query para carga inicial de datos y respaldo cuando el socket se desconecta.

Sí. TanStack Query 5+ es completamente compatible con React 19. Las APIs usadas aquí (consultas, mutaciones, invalidación) no han cambiado, por lo que los ejemplos en este artículo funcionarán sin modificación.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers