effidevFlutter · Edge de Cloudflare · Optimización de costes en la nube
Español

TanStack Query v5 en la práctica: Actualizaciones optimistas y patrones de almacenamiento en caché sin conexión

Diagrama de arquitectura de actualizaciones optimistas y almacenamiento en caché sin conexión en TanStack Query v5

En las aplicaciones web modernas, la experiencia del usuario (UX) es el factor central que determina el éxito de una aplicación. Imagínese si la interfaz de usuario reaccionara instantáneamente en lugar de mostrar un indicador de carga cuando el usuario hace clic en un botón.

En este artículo, analizaremos en profundidad cómo implementar actualizaciones optimistas (Optimistic Updates) para ofrecer una experiencia sin latencia utilizando TanStack Query v5 (anteriormente React Query), y exploraremos patrones de almacenamiento en caché sin conexión (Offline Caching) para cuando se pierde la conexión de red.

1. ¿Qué son las actualizaciones optimistas?

Las actualizaciones optimistas son una técnica en la que la interfaz de usuario se actualiza inmediatamente, asumiendo con optimismo que la solicitud tendrá éxito, sin esperar la respuesta del servidor.

En TanStack Query v5, puede manejar este mecanismo de reversión de manera elegante a través de las funciones de ciclo de vida del hook useMutation (onMutate, onError, onSettled).

[!NOTE] En la v5, el objeto de contexto devuelto por onMutate se pasa a onError y onSettled para ser utilizado en las reversiones.

2. Implementación de actualizaciones optimistas con TanStack Query v5

Veamos un ejemplo sencillo de la función de alternar ‘Me gusta’.

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

// Definición del tipo Post
interface Post {
  id: string;
  title: string;
  likes: number;
}

export function useToggleLike() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (postId: string) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
    
    // onMutate: Se llama antes de que se ejecute la función de mutación
    onMutate: async (newPostId) => {
      // 1. Cancelar cualquier consulta en curso para evitar sobrescribir nuestra actualización optimista
      await queryClient.cancelQueries({ queryKey: ['posts', newPostId] });

      // 2. Guardar una captura (snapshot) del estado anterior para recuperarlo en caso de error
      const previousPost = queryClient.getQueryData<Post>(['posts', newPostId]);

      // 3. Actualizar la caché de forma optimista
      if (previousPost) {
        queryClient.setQueryData<Post>(['posts', newPostId], {
          ...previousPost,
          likes: previousPost.likes + 1,
        });
      }

      // 4. Devolver un contexto con los datos anteriores
      return { previousPost };
    },
    
    // onError: Revertir al estado anterior usando el contexto devuelto por onMutate
    onError: (err, newPostId, context) => {
      if (context?.previousPost) {
        queryClient.setQueryData(['posts', newPostId], context.previousPost);
      }
    },
    
    // onSettled: Invalidar las consultas para asegurar datos frescos, ya sea por éxito o fracaso
    onSettled: (newPostId) => {
      queryClient.invalidateQueries({ queryKey: ['posts', newPostId] });
    },
  });
}

Análisis de los puntos clave

  1. cancelQueries: Evita que las actualizaciones optimistas sean sobrescritas por datos obsoletos que llegan de una solicitud demorada.
  2. Instantánea previousPost: Guarda el estado original antes de modificar la caché.
  3. Devolución de contexto: El valor devuelto por onMutate se pasa como tercer argumento a onError para usarlo en la reversión.

3. Prioridad sin conexión (Offline First) y patrones de caché

Para evitar que los usuarios vean una pantalla en blanco cuando están desconectados o en una red inestable, se requiere una estrategia de caché adecuada. TanStack Query v5 proporciona potentes mecanismos para escenarios sin conexión por defecto.

3.1 Configuración del Network Mode

El networkMode predeterminado de TanStack Query es online. Esto significa que si no hay conexión de red, las consultas y mutaciones se pausarán.

Si está desarrollando una aplicación ‘offline-first’, se recomienda utilizar la configuración offlineFirst para mostrar datos en caché e intentar sincronizar al volver a conectarse.

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst',
      staleTime: 1000 * 60 * 5, // 5 minutos
    },
  },
});

3.2 Caché persistente con Persisters

Para retener datos incluso después de actualizar o cerrar y reabrir el navegador, se necesita un plugin Persister. En la v5, se usan comúnmente persisters asincrónicos basados en @tanstack/query-sync-storage-persister o idb-keyval (IndexedDB).

Ejemplo de persistencia de caché usando IndexedDB:

import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { get, set, del } from 'idb-keyval';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // Mantener la caché por 24 horas
    },
  },
});

const indexedDBPersister = createAsyncStoragePersister({
  storage: {
    getItem: async (key) => await get(key),
    setItem: async (key, value) => await set(key, value),
    removeItem: async (key) => await del(key),
  },
});

// Envolver con Provider en la parte superior de su aplicación
function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister: indexedDBPersister }}
    >
      <YourApp />
    </PersistQueryClientProvider>
  );
}

4. Casos límite prácticos y decisiones de arquitectura

Al aplicar actualizaciones optimistas y caché sin conexión en producción, hay algunos casos extremos que deben tenerse en cuenta.

4.1 Actualizaciones optimistas consecutivas rápidas

Piense en un escenario en el que un usuario hace clic rápidamente en el botón ‘Me gusta’ varias veces. Si onMutate se dispara cada vez, la instantánea podría capturar un estado intermedio.

Soluciones:

Problema Causa Patrón de solución
Fallo en la reversión Falta la captura de caché antes de onMutate Devuelva siempre queryClient.getQueryData como contexto
Sobrescritura de datos Una petición GET en curso llega después del POST Coloque queryClient.cancelQueries al principio de onMutate
Parpadeo al agregar a lista Discrepancia entre ID temporal y del servidor Reemplace el objeto temporal con la respuesta del servidor usando map

Resumen