Cache y Invalidation
Cache y Invalidation: El Corazón de TanStack Query
Section titled “Cache y Invalidation: El Corazón de TanStack Query”El sistema de cache de TanStack Query es su característica más poderosa. Entender cómo funciona y cómo controlarlo te permitirá crear aplicaciones increíblemente rápidas y eficientes.
🧠 Entendiendo el Cache
Section titled “🧠 Entendiendo el Cache”Estructura del Cache
Section titled “Estructura del Cache”El cache de TanStack Query funciona como un mapa donde cada query se identifica por su queryKey
:
// Cache interno (conceptual){ "['posts']": { data: [...], dataUpdatedAt: 1640995200000, isStale: false, isInvalidated: false }, "['user', 123]": { data: { id: 123, name: 'Juan' }, dataUpdatedAt: 1640995300000, isStale: true, isInvalidated: false }}
Estados del Cache
Section titled “Estados del Cache”Cada entry del cache tiene varios estados importantes:
function CacheStatesDemo() { const queryClient = useQueryClient();
const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 5 * 60 * 1000 // 5 minutos });
const inspectCacheState = () => { const queryState = queryClient.getQueryState(['posts']);
console.log({ data: queryState.data, dataUpdatedAt: new Date(queryState.dataUpdatedAt), isStale: queryState.isStale, isInvalidated: queryState.isInvalidated, isLoading: queryState.isLoading, status: queryState.status, // 'idle' | 'loading' | 'error' | 'success' fetchStatus: queryState.fetchStatus // 'idle' | 'fetching' | 'paused' }); };
return ( <div> <button onClick={inspectCacheState}> 🔍 Inspeccionar Estado del Cache </button> {/* Render posts */} </div> );}
⏰ Stale Time vs Cache Time
Section titled “⏰ Stale Time vs Cache Time”Dos conceptos fundamentales que a menudo se confunden:
Stale Time
Section titled “Stale Time”Tiempo que los datos se consideran “frescos”:
const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 5 * 60 * 1000, // 5 minutos});
// Comportamiento:// - 0-5 min: Datos "fresh" - no se refetch automáticamente// - 5+ min: Datos "stale" - se refetch en background focus, mount, etc.
Cache Time
Section titled “Cache Time”Tiempo que los datos permanecen en memoria después de que no se usan:
const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, cacheTime: 10 * 60 * 1000, // 10 minutos});
// Comportamiento:// - Mientras el componente está montado: datos en cache// - Después de unmount: esperar 10 minutos antes de limpiar cache// - Si remonta antes de 10 min: usa datos del cache inmediatamente
Ejemplo Comparativo
Section titled “Ejemplo Comparativo”function StaleVsCacheDemo() { const [showPosts, setShowPosts] = useState(true);
return ( <div> <button onClick={() => setShowPosts(!showPosts)}> {showPosts ? 'Ocultar' : 'Mostrar'} Posts </button>
{showPosts && <PostsComponent />} </div> );}
function PostsComponent() { const { data, isStale, isFetching } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 30 * 1000, // 30 segundos fresh cacheTime: 5 * 60 * 1000, // 5 minutos en cache });
useEffect(() => { console.log('Posts component mounted'); return () => console.log('Posts component unmounted'); }, []);
return ( <div> <div> Stale: {isStale ? '🔄 Obsoleto' : '✅ Fresco'} | Fetching: {isFetching ? '📡 Cargando' : '💤 Idle'} </div> {/* Render posts */} </div> );}
// Flujo de comportamiento:// 1. Mount: fetch initial, fresh por 30s// 2. 30s+: se marca stale, refetch en background en focus/mount// 3. Unmount: datos permanecen en cache por 5 minutos// 4. Remount <5min: datos del cache instantáneamente, pero refetch si stale// 5. Remount >5min: cache vacío, fetch desde cero
🎯 Estrategias de Invalidación
Section titled “🎯 Estrategias de Invalidación”1. Invalidación Manual
Section titled “1. Invalidación Manual”function ManualInvalidation() { const queryClient = useQueryClient();
const invalidateAllPosts = () => { // Invalidar todas las queries de posts queryClient.invalidateQueries({ queryKey: ['posts'] }); };
const invalidateSpecificPost = (postId) => { // Invalidar post específico queryClient.invalidateQueries({ queryKey: ['post', postId] }); };
const invalidateUserData = (userId) => { // Invalidar múltiples queries relacionadas con un usuario queryClient.invalidateQueries({ queryKey: ['user', userId] }); queryClient.invalidateQueries({ queryKey: ['posts', 'user', userId] }); queryClient.invalidateQueries({ queryKey: ['comments', 'user', userId] }); };
const invalidateStaleOnly = () => { // Solo invalidar queries que ya están stale queryClient.invalidateQueries({ stale: true, refetchType: 'active' // solo las activas (componentes montados) }); };
return ( <div> <button onClick={invalidateAllPosts}> 🔄 Invalidar Todos los Posts </button> <button onClick={() => invalidateSpecificPost(1)}> 🔄 Invalidar Post 1 </button> <button onClick={() => invalidateUserData(123)}> 🔄 Invalidar Datos Usuario 123 </button> <button onClick={invalidateStaleOnly}> 🔄 Invalidar Solo Stale </button> </div> );}
2. Invalidación Automática tras Mutaciones
Section titled “2. Invalidación Automática tras Mutaciones”function AutoInvalidationAfterMutation() { const queryClient = useQueryClient();
// Crear post const createPostMutation = useMutation({ mutationFn: createPost, onSuccess: (newPost) => { // Estrategia 1: Invalidar lista de posts queryClient.invalidateQueries({ queryKey: ['posts'] });
// Estrategia 2: Agregar al cache directamente (más rápido) queryClient.setQueryData(['posts'], (oldPosts) => [newPost, ...oldPosts]);
// Estrategia 3: Invalidar queries relacionadas con el usuario queryClient.invalidateQueries({ queryKey: ['posts', 'user', newPost.userId] }); } });
// Actualizar post const updatePostMutation = useMutation({ mutationFn: updatePost, onSuccess: (updatedPost, variables) => { // Actualizar post específico queryClient.setQueryData(['post', variables.id], updatedPost);
// Invalidar listas que puedan contener este post queryClient.invalidateQueries({ queryKey: ['posts'] }); queryClient.invalidateQueries({ queryKey: ['posts', 'user', updatedPost.userId] }); } });
// Eliminar post const deletePostMutation = useMutation({ mutationFn: deletePost, onSuccess: (_, deletedId) => { // Remover query específica queryClient.removeQueries({ queryKey: ['post', deletedId] });
// Actualizar listas removiendo el post queryClient.setQueriesData( { queryKey: ['posts'] }, (oldPosts) => oldPosts?.filter(post => post.id !== deletedId) ); } });
return ( <div> <button onClick={() => createPostMutation.mutate({ title: 'Nuevo Post' })}> ➕ Crear Post </button> <button onClick={() => updatePostMutation.mutate({ id: 1, title: 'Actualizado' })}> ✏️ Actualizar Post 1 </button> <button onClick={() => deletePostMutation.mutate(1)}> 🗑️ Eliminar Post 1 </button> </div> );}
3. Invalidación Basada en Tiempo
Section titled “3. Invalidación Basada en Tiempo”function TimeBasedInvalidation() { const queryClient = useQueryClient();
useEffect(() => { // Invalidar datos cada 5 minutos const interval = setInterval(() => { queryClient.invalidateQueries({ queryKey: ['realtime-data'], refetchType: 'active' }); }, 5 * 60 * 1000);
return () => clearInterval(interval); }, [queryClient]);
// Invalidar cuando la ventana regaine focus después de mucho tiempo useEffect(() => { let lastFocusTime = Date.now();
const handleFocus = () => { const timeSinceFocus = Date.now() - lastFocusTime; const fiveMinutes = 5 * 60 * 1000;
if (timeSinceFocus > fiveMinutes) { // Han pasado más de 5 minutos, invalidar datos críticos queryClient.invalidateQueries({ queryKey: ['critical-data'], refetchType: 'active' }); }
lastFocusTime = Date.now(); };
const handleBlur = () => { lastFocusTime = Date.now(); };
window.addEventListener('focus', handleFocus); window.addEventListener('blur', handleBlur);
return () => { window.removeEventListener('focus', handleFocus); window.removeEventListener('blur', handleBlur); }; }, [queryClient]);
return <div>Invalidación automática configurada</div>;}
🔄 Sincronización de Cache
Section titled “🔄 Sincronización de Cache”1. Propagación de Cambios
Section titled “1. Propagación de Cambios”function CachePropagation() { const queryClient = useQueryClient();
const updateUserMutation = useMutation({ mutationFn: updateUser, onSuccess: (updatedUser) => { // 1. Actualizar usuario específico queryClient.setQueryData(['user', updatedUser.id], updatedUser);
// 2. Actualizar usuario en lista de usuarios queryClient.setQueryData(['users'], (oldUsers) => oldUsers?.map(user => user.id === updatedUser.id ? updatedUser : user ) );
// 3. Actualizar posts del usuario si el nombre cambió if (updatedUser.name) { queryClient.setQueriesData( { queryKey: ['posts', 'user', updatedUser.id] }, (oldPosts) => oldPosts?.map(post => ({ ...post, authorName: updatedUser.name })) ); }
// 4. Invalidar queries que dependen de este usuario queryClient.invalidateQueries({ predicate: (query) => { // Invalidar cualquier query que incluya este userId return query.queryKey.includes(updatedUser.id); } }); } });
return ( <button onClick={() => updateUserMutation.mutate({ id: 1, name: 'Nuevo Nombre' })}> Actualizar Usuario </button> );}
2. Normalización de Datos
Section titled “2. Normalización de Datos”// Utility para normalizar datos relacionadosfunction normalizePostsWithUsers(posts, users) { const userMap = new Map(users.map(user => [user.id, user]));
return posts.map(post => ({ ...post, author: userMap.get(post.userId) || null }));}
function NormalizedDataExample() { const queryClient = useQueryClient();
// Fetch posts y users por separado const { data: posts } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const { data: users } = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
// Combinar datos usando select const { data: normalizedPosts } = useQuery({ queryKey: ['posts', 'normalized'], queryFn: () => Promise.resolve(null), // No fetch real select: () => { const cachedPosts = queryClient.getQueryData(['posts']); const cachedUsers = queryClient.getQueryData(['users']);
if (cachedPosts && cachedUsers) { return normalizePostsWithUsers(cachedPosts, cachedUsers); } return []; }, enabled: !!(posts && users), // Solo cuando ambos están disponibles });
return ( <div> {normalizedPosts?.map(post => ( <div key={post.id}> <h3>{post.title}</h3> <p>Por: {post.author?.name || 'Autor desconocido'}</p> </div> ))} </div> );}
🎨 Patterns Avanzados de Cache
Section titled “🎨 Patterns Avanzados de Cache”1. Cache Warming
Section titled “1. Cache Warming”Pre-poblar el cache con datos conocidos:
function CacheWarming() { const queryClient = useQueryClient();
const warmUpCache = async () => { // Warm up datos críticos await Promise.all([ queryClient.prefetchQuery({ queryKey: ['user', 'current'], queryFn: fetchCurrentUser, staleTime: 10 * 60 * 1000 }), queryClient.prefetchQuery({ queryKey: ['notifications'], queryFn: fetchNotifications, staleTime: 2 * 60 * 1000 }), queryClient.prefetchQuery({ queryKey: ['dashboard-stats'], queryFn: fetchDashboardStats, staleTime: 5 * 60 * 1000 }) ]); };
// Warm up en mount de la app useEffect(() => { warmUpCache(); }, []);
// Warm up basado en interacciones const handleUserHover = (userId) => { queryClient.prefetchQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), staleTime: 5 * 60 * 1000 }); };
return ( <div> <button onClick={warmUpCache}> 🔥 Warm Up Cache </button> </div> );}
2. Cache Patterns por Módulo
Section titled “2. Cache Patterns por Módulo”export const cachePatterns = { // Invalidar todos los datos de un usuario invalidateUserData: (queryClient, userId) => { queryClient.invalidateQueries({ predicate: (query) => { const key = query.queryKey; return ( key.includes('user') && key.includes(userId) || key.includes('posts') && key.includes(userId) || key.includes('comments') && key.includes(userId) ); } }); },
// Actualizar usuario en todas las ubicaciones updateUserEverywhere: (queryClient, updatedUser) => { // Usuario individual queryClient.setQueryData(['user', updatedUser.id], updatedUser);
// Lista de usuarios queryClient.setQueryData(['users'], (oldUsers) => oldUsers?.map(user => user.id === updatedUser.id ? updatedUser : user ) );
// Posts del usuario queryClient.setQueriesData( { queryKey: ['posts'] }, (oldPosts) => oldPosts?.map(post => post.userId === updatedUser.id ? { ...post, authorName: updatedUser.name } : post ) ); },
// Limpiar cache obsoleto cleanupStaleCache: (queryClient) => { queryClient.removeQueries({ type: 'inactive', stale: true, predicate: (query) => { const age = Date.now() - query.state.dataUpdatedAt; const oneHour = 60 * 60 * 1000; return age > oneHour; // Remover datos inactivos de más de 1 hora } }); }};
// Uso en componentesfunction UserManagement() { const queryClient = useQueryClient();
const updateUserMutation = useMutation({ mutationFn: updateUser, onSuccess: (updatedUser) => { cachePatterns.updateUserEverywhere(queryClient, updatedUser); } });
const cleanupCache = () => { cachePatterns.cleanupStaleCache(queryClient); };
return ( <div> <button onClick={cleanupCache}> 🧹 Limpiar Cache </button> </div> );}
3. Cache Debugging y Monitoring
Section titled “3. Cache Debugging y Monitoring”function CacheMonitor() { const queryClient = useQueryClient(); const [cacheStats, setCacheStats] = useState({});
const updateStats = useCallback(() => { const cache = queryClient.getQueryCache(); const queries = cache.getAll();
const stats = { total: queries.length, active: queries.filter(q => q.isActive()).length, stale: queries.filter(q => q.isStale()).length, loading: queries.filter(q => q.state.isFetching).length, errors: queries.filter(q => q.state.isError).length, memoryUsage: estimateCacheSize(queries) };
setCacheStats(stats); }, [queryClient]);
// Función para estimar el tamaño del cache const estimateCacheSize = (queries) => { let totalSize = 0; queries.forEach(query => { if (query.state.data) { totalSize += JSON.stringify(query.state.data).length; } }); return totalSize; };
useEffect(() => { updateStats(); const interval = setInterval(updateStats, 5000); // Actualizar cada 5 segundos return () => clearInterval(interval); }, [updateStats]);
return ( <div style={{ position: 'fixed', top: '10px', right: '10px', background: 'white', padding: '10px', border: '1px solid #ccc', borderRadius: '8px', fontSize: '12px', zIndex: 9999 }}> <h4>📊 Cache Stats</h4> <div>Total: {cacheStats.total}</div> <div>Active: {cacheStats.active}</div> <div>Stale: {cacheStats.stale}</div> <div>Loading: {cacheStats.loading}</div> <div>Errors: {cacheStats.errors}</div> <div>Size: ~{Math.round(cacheStats.memoryUsage / 1024)}KB</div>
<button onClick={updateStats} style={{ marginTop: '5px', fontSize: '10px' }} > 🔄 Refresh </button> </div> );}
✅ Mejores Prácticas para Cache
Section titled “✅ Mejores Prácticas para Cache”- Configura staleTime apropiadamente: Basado en qué tan frecuentemente cambian tus datos
- Usa invalidación específica: En lugar de invalidar todo
- Implementa cache warming: Para datos críticos
- Normaliza datos relacionados: Para evitar inconsistencias
- Monitorea el tamaño del cache: Para prevenir uso excesivo de memoria
- Usa predicados para invalidación: Para queries complejas
- Implementa cleanup periódico: Para cache inactivo
- Considera la propagación de cambios: Actualiza datos relacionados
- Usa refetchType apropiado: ‘active’, ‘inactive’, o ‘all’
- Debug con DevTools: Para entender el comportamiento del cache
Ahora que dominas el cache, exploremos las actualizaciones optimistas para mejorar la experiencia del usuario.
Próximo paso: Optimistic Updates