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

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.
- Ventajas: Los usuarios no perciben la latencia de la red y sienten que la aplicación es extremadamente rápida.
- Desventajas: Añade complejidad al tener que revertir (rollback) la interfaz de usuario a su estado anterior si la solicitud del servidor falla.
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
onMutatese pasa aonErroryonSettledpara 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
cancelQueries: Evita que las actualizaciones optimistas sean sobrescritas por datos obsoletos que llegan de una solicitud demorada.- Instantánea
previousPost: Guarda el estado original antes de modificar la caché. - Devolución de contexto: El valor devuelto por
onMutatese pasa como tercer argumento aonErrorpara 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.
online(predeterminado): Pausar solicitudes hasta que la red se vuelva a conectar.always: Siempre intenta buscar y actualizar la caché, ignorando el estado de la red.offlineFirst: Prioriza el uso de la caché sin conexión mientras realiza la sincronización en segundo plano.
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:
- Aplicar ‘debouncing’ en el lado de la interfaz de usuario.
- Administrar una cola de actualización localmente antes de mutar la caché.
- Para datos de matrices (por ejemplo, agregar un Todo), emitir un ID temporal (
uuid) y reemplazarlo con el ID real después de que el servidor responda.
| 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
- Las actualizaciones optimistas mejoran drásticamente la UX al dar a los usuarios una sensación de respuesta inmediata.
- Mantenga constantemente el patrón de guardar una instantánea en
onMutatey revertir enonError. - Para lograr una prioridad offline (offline first), combine el modo de red
offlineFirstcon los Persisters basados en IndexedDB para crear aplicaciones web sólidas. - Diseñe con antelación para casos extremos como eventos consecutivos o gestión de identificadores para mantener la integridad de los datos.