Background Refetching
Background Refetching: Datos Siempre Frescos
Section titled “Background Refetching: Datos Siempre Frescos”El background refetching es una de las características más poderosas de TanStack Query. Permite mantener los datos actualizados automáticamente sin interrumpir la experiencia del usuario, actualizando en segundo plano de forma inteligente.
🎯 ¿Qué es Background Refetching?
Section titled “🎯 ¿Qué es Background Refetching?”Es el proceso de actualizar datos automáticamente en segundo plano cuando:
- La ventana recupera el foco
- Se reconecta la conexión a internet
- Se monta un componente con datos obsoletos
- Transcurre un intervalo de tiempo específico
// Los datos se refetch automáticamente sin mostrar loadingconst { data: posts, isFetching } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, staleTime: 5 * 60 * 1000, // Datos fresh por 5 minutos refetchOnWindowFocus: true, // ✅ Refetch al enfocar ventana refetchOnReconnect: true, // ✅ Refetch al reconectar refetchInterval: 30000, // ✅ Refetch cada 30 segundos});
// isFetching será true durante el refetch, pero data mantiene el valor anterior
🔄 Tipos de Background Refetching
Section titled “🔄 Tipos de Background Refetching”1. Window Focus Refetching
Section titled “1. Window Focus Refetching”Actualiza datos cuando el usuario regresa a la pestaña:
function WindowFocusExample() { const { data: notifications, isFetching } = useQuery({ queryKey: ['notifications'], queryFn: fetchNotifications, staleTime: 2 * 60 * 1000, // 2 minutos refetchOnWindowFocus: true, // Por defecto es true });
return ( <div> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <h2>🔔 Notificaciones</h2> {isFetching && <span>🔄 Actualizando...</span>} </div>
<div> {notifications?.map(notification => ( <div key={notification.id} style={{ padding: '10px', margin: '5px 0', backgroundColor: notification.read ? '#f8f9fa' : '#fff3cd', borderRadius: '4px' }}> <strong>{notification.title}</strong> <p>{notification.message}</p> <small>{new Date(notification.createdAt).toLocaleString()}</small> </div> ))} </div>
<div style={{ marginTop: '20px', fontSize: '14px', color: '#666' }}> 💡 Cambia a otra pestaña y regresa - las notificaciones se actualizarán automáticamente </div> </div> );}
2. Network Reconnection Refetching
Section titled “2. Network Reconnection Refetching”Actualiza datos cuando se recupera la conexión:
function NetworkReconnectExample() { const [isOnline, setIsOnline] = useState(navigator.onLine);
const { data: criticalData, isFetching } = useQuery({ queryKey: ['critical-data'], queryFn: fetchCriticalData, refetchOnReconnect: true, // Por defecto es true retry: (failureCount, error) => { // Reintentar más agresivamente después de reconexión return !isOnline ? false : failureCount < 3; } });
useEffect(() => { const handleOnline = () => setIsOnline(true); const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline);
return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []);
return ( <div> <div style={{ padding: '10px', backgroundColor: isOnline ? '#d4edda' : '#f8d7da', borderRadius: '4px', marginBottom: '20px' }}> Estado: {isOnline ? '🟢 Online' : '🔴 Offline'} {isFetching && ' - 🔄 Sincronizando...'} </div>
<div> <h3>📊 Datos Críticos</h3> {criticalData ? ( <pre>{JSON.stringify(criticalData, null, 2)}</pre> ) : ( <div>No hay datos disponibles</div> )} </div>
<div style={{ marginTop: '20px', fontSize: '14px', color: '#666' }}> 💡 Desconecta tu internet y reconéctalo - los datos se actualizarán automáticamente </div> </div> );}
3. Interval Refetching
Section titled “3. Interval Refetching”Actualiza datos en intervalos regulares:
function IntervalRefetchingExample() { const [intervalEnabled, setIntervalEnabled] = useState(true);
const { data: realtimeData, isFetching, dataUpdatedAt } = useQuery({ queryKey: ['realtime-stats'], queryFn: fetchRealtimeStats, refetchInterval: intervalEnabled ? 5000 : false, // 5 segundos refetchIntervalInBackground: true, // Continuar incluso sin foco });
const { data: slowData } = useQuery({ queryKey: ['slow-changing-data'], queryFn: fetchSlowChangingData, refetchInterval: 60000, // 1 minuto refetchIntervalInBackground: false, // Solo si la ventana tiene foco });
return ( <div style={{ padding: '20px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '15px', marginBottom: '20px' }}> <h2>📈 Datos en Tiempo Real</h2> {isFetching && <span>🔄</span>} <label> <input type="checkbox" checked={intervalEnabled} onChange={(e) => setIntervalEnabled(e.target.checked)} /> Auto-actualizar cada 5s </label> </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}> <div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}> <h3>⚡ Actualización Rápida (5s)</h3> <div>Usuarios activos: {realtimeData?.activeUsers || 0}</div> <div>Ventas hoy: ${realtimeData?.todaySales || 0}</div> <div>Última actualización: { dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString() : 'Nunca' }</div> </div>
<div style={{ padding: '15px', border: '1px solid #ddd', borderRadius: '8px' }}> <h3>🐌 Actualización Lenta (1min)</h3> <div>Total usuarios: {slowData?.totalUsers || 0}</div> <div>Ingresos mes: ${slowData?.monthlyRevenue || 0}</div> <div style={{ fontSize: '12px', color: '#666' }}> (Solo se actualiza con ventana enfocada) </div> </div> </div>
<div style={{ marginTop: '20px', fontSize: '14px', color: '#666' }}> 💡 Los datos de la izquierda se actualizan cada 5 segundos incluso sin foco. Los de la derecha solo cuando la ventana está enfocada. </div> </div> );}
🎛️ Configuración Avanzada
Section titled “🎛️ Configuración Avanzada”Refetching Condicional
Section titled “Refetching Condicional”function ConditionalRefetching({ userId, isImportantUser }) { const { data: userData } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId),
// Configuración dinámica basada en condiciones refetchOnWindowFocus: isImportantUser, // Solo usuarios importantes refetchInterval: isImportantUser ? 10000 : 60000, // Más frecuente para VIPs refetchIntervalInBackground: isImportantUser,
// Refetch condicional basado en datos refetchOnMount: (query) => { const isStale = query.isStale(); const dataAge = Date.now() - (query.state.dataUpdatedAt || 0); const fiveMinutes = 5 * 60 * 1000;
return isStale || dataAge > fiveMinutes; } });
const { data: activities } = useQuery({ queryKey: ['activities', userId], queryFn: () => fetchUserActivities(userId),
// Solo refetch si el usuario está activo enabled: !!userData, refetchOnWindowFocus: userData?.isActive, refetchInterval: userData?.isActive ? 30000 : false, });
return ( <div> <h3>Usuario: {userData?.name}</h3> <div>Estado: {userData?.isActive ? '🟢 Activo' : '⚫ Inactivo'}</div> {isImportantUser && <div>⭐ Usuario VIP - Datos actualizados frecuentemente</div>}
<div style={{ marginTop: '20px' }}> <h4>Actividades Recientes:</h4> {activities?.map(activity => ( <div key={activity.id}>{activity.description}</div> ))} </div> </div> );}
Smart Refetching basado en Visibilidad
Section titled “Smart Refetching basado en Visibilidad”function VisibilityBasedRefetching() { const [isVisible, setIsVisible] = useState(true); const elementRef = useRef();
// Hook personalizado para detectar visibilidad useEffect(() => { const observer = new IntersectionObserver( ([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 } );
if (elementRef.current) { observer.observe(elementRef.current); }
return () => observer.disconnect(); }, []);
const { data: liveChart, isFetching } = useQuery({ queryKey: ['live-chart'], queryFn: fetchLiveChartData, refetchInterval: isVisible ? 2000 : false, // Solo refetch si es visible refetchIntervalInBackground: false, });
return ( <div ref={elementRef} style={{ minHeight: '400px', border: '2px solid #ddd', borderRadius: '8px', padding: '20px', backgroundColor: isVisible ? '#f8f9fa' : '#e9ecef' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <h3>📊 Gráfico en Tiempo Real</h3> <div> {isVisible ? '👁️ Visible' : '🙈 Oculto'} {isFetching && ' - 🔄 Actualizando'} </div> </div>
<div style={{ height: '300px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> {liveChart ? ( <div> {/* Simular gráfico */} <div>Datos del gráfico: {JSON.stringify(liveChart.data.slice(0, 3))}...</div> <div>Última actualización: {new Date(liveChart.timestamp).toLocaleTimeString()}</div> </div> ) : ( <div>Cargando gráfico...</div> )} </div>
<div style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}> 💡 Este gráfico solo se actualiza cuando es visible en pantalla. Haz scroll hacia abajo y luego regresa para ver cómo se pausa/reanuda. </div> </div> );}
🔄 Refetching Manual y Programático
Section titled “🔄 Refetching Manual y Programático”Refetch Trigger Manual
Section titled “Refetch Trigger Manual”function ManualRefetchControl() { const queryClient = useQueryClient();
const { data: posts, isFetching, refetch, dataUpdatedAt } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, refetchOnWindowFocus: false, // Deshabilitamos auto-refetch refetchOnReconnect: false, refetchInterval: false, });
const [autoRefresh, setAutoRefresh] = useState(false);
// Auto-refresh controlado manualmente useEffect(() => { if (!autoRefresh) return;
const interval = setInterval(() => { refetch(); }, 10000); // 10 segundos
return () => clearInterval(interval); }, [autoRefresh, refetch]);
const handleForceRefresh = () => { refetch(); };
const handleRefreshAll = () => { // Refetch múltiples queries queryClient.refetchQueries({ type: 'active', // Solo las activas stale: true // Solo las obsoletas }); };
const handleSmartRefresh = () => { // Refetch solo si los datos son antiguos const dataAge = Date.now() - (dataUpdatedAt || 0); const fiveMinutes = 5 * 60 * 1000;
if (dataAge > fiveMinutes) { refetch(); } else { alert('Los datos son recientes, no es necesario actualizar'); } };
return ( <div style={{ padding: '20px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '15px', marginBottom: '20px' }}> <h2>🎛️ Control Manual de Refetch</h2> {isFetching && <span>🔄 Actualizando...</span>} </div>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px', flexWrap: 'wrap' }}> <button onClick={handleForceRefresh} disabled={isFetching} style={{ padding: '8px 16px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }} > 🔄 Refetch Manual </button>
<button onClick={handleRefreshAll} style={{ padding: '8px 16px', backgroundColor: '#28a745', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }} > 🔄 Refetch Todo </button>
<button onClick={handleSmartRefresh} style={{ padding: '8px 16px', backgroundColor: '#ffc107', color: 'black', border: 'none', borderRadius: '4px', cursor: 'pointer' }} > 🧠 Refetch Inteligente </button>
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}> <input type="checkbox" checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} /> Auto-refresh cada 10s </label> </div>
<div style={{ marginBottom: '20px', fontSize: '14px', color: '#666' }}> Última actualización: { dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleString() : 'Nunca' } </div>
<div> <h3>📝 Posts ({posts?.length || 0})</h3> {posts?.slice(0, 5).map(post => ( <div key={post.id} style={{ padding: '10px', margin: '5px 0', backgroundColor: '#f8f9fa', borderRadius: '4px' }}> <strong>{post.title}</strong> <p>{post.body.substring(0, 100)}...</p> </div> ))} </div> </div> );}
📊 Dashboard con Múltiples Estrategias
Section titled “📊 Dashboard con Múltiples Estrategias”function ComprehensiveDashboard() { const [focusRefetch, setFocusRefetch] = useState(true); const [intervalRefetch, setIntervalRefetch] = useState(false);
// Datos críticos - refetch agresivo const { data: criticalMetrics, isFetching: criticalFetching } = useQuery({ queryKey: ['critical-metrics'], queryFn: fetchCriticalMetrics, staleTime: 30 * 1000, // 30 segundos refetchInterval: 15000, // 15 segundos refetchOnWindowFocus: true, refetchOnReconnect: true, });
// Datos normales - refetch moderado const { data: normalMetrics, isFetching: normalFetching } = useQuery({ queryKey: ['normal-metrics'], queryFn: fetchNormalMetrics, staleTime: 2 * 60 * 1000, // 2 minutos refetchInterval: intervalRefetch ? 60000 : false, // 1 minuto si está habilitado refetchOnWindowFocus: focusRefetch, refetchOnReconnect: true, });
// Datos históricos - refetch mínimo const { data: historicalData, isFetching: historicalFetching } = useQuery({ queryKey: ['historical-data'], queryFn: fetchHistoricalData, staleTime: 10 * 60 * 1000, // 10 minutos refetchOnWindowFocus: false, refetchOnReconnect: false, refetchInterval: false, });
return ( <div style={{ padding: '20px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}> <h1>📊 Dashboard Completo</h1>
<div style={{ display: 'flex', gap: '15px', fontSize: '14px' }}> <label> <input type="checkbox" checked={focusRefetch} onChange={(e) => setFocusRefetch(e.target.checked)} /> Refetch al enfocar </label> <label> <input type="checkbox" checked={intervalRefetch} onChange={(e) => setIntervalRefetch(e.target.checked)} /> Refetch por intervalo </label> </div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: '20px' }}> {/* Métricas Críticas */} <div style={{ padding: '20px', border: '2px solid #dc3545', borderRadius: '8px', backgroundColor: '#fff5f5' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <h3>🚨 Críticas</h3> {criticalFetching && <span>🔄</span>} </div> <div>Errores: {criticalMetrics?.errors || 0}</div> <div>Tiempo respuesta: {criticalMetrics?.responseTime || 0}ms</div> <div>CPU: {criticalMetrics?.cpuUsage || 0}%</div> <div style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}> ⚡ Actualización cada 15s automáticamente </div> </div>
{/* Métricas Normales */} <div style={{ padding: '20px', border: '2px solid #007bff', borderRadius: '8px', backgroundColor: '#f8f9ff' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <h3>📈 Normales</h3> {normalFetching && <span>🔄</span>} </div> <div>Usuarios activos: {normalMetrics?.activeUsers || 0}</div> <div>Páginas vistas: {normalMetrics?.pageViews || 0}</div> <div>Sesiones: {normalMetrics?.sessions || 0}</div> <div style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}> 🔄 Refetch configureable en opciones </div> </div>
{/* Datos Históricos */} <div style={{ padding: '20px', border: '2px solid #6c757d', borderRadius: '8px', backgroundColor: '#f8f9fa' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <h3>📚 Históricos</h3> {historicalFetching && <span>🔄</span>} </div> <div>Total usuarios: {historicalData?.totalUsers || 0}</div> <div>Ingresos mes: ${historicalData?.monthlyRevenue || 0}</div> <div>Crecimiento: {historicalData?.growth || 0}%</div> <div style={{ fontSize: '12px', color: '#666', marginTop: '10px' }}> 💤 Solo refetch manual - datos estables </div> </div> </div>
<div style={{ marginTop: '30px', padding: '15px', backgroundColor: '#e7f3ff', borderRadius: '8px', fontSize: '14px' }}> <h4>💡 Estrategias de Refetching:</h4> <ul style={{ marginBottom: 0 }}> <li><strong>Críticas:</strong> Refetch cada 15s + al enfocar + al reconectar</li> <li><strong>Normales:</strong> Refetch configurable + condicional al enfocar</li> <li><strong>Históricos:</strong> Sin refetch automático - solo manual</li> </ul> </div> </div> );}
🎯 Patterns Avanzados
Section titled “🎯 Patterns Avanzados”Refetch en Cascada
Section titled “Refetch en Cascada”function CascadingRefetch({ userId }) { const queryClient = useQueryClient();
// Query principal const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), refetchOnWindowFocus: true, });
// Queries dependientes que se refetch cuando la principal se actualiza const { data: userPosts } = useQuery({ queryKey: ['posts', 'user', userId], queryFn: () => fetchUserPosts(userId), enabled: !!user, // Se actualizará automáticamente cuando user se refetch });
const { data: userStats } = useQuery({ queryKey: ['stats', 'user', userId], queryFn: () => fetchUserStats(userId), enabled: !!user, });
// Trigger manual para refetch todo const refetchUserData = () => { // Refetch en orden específico queryClient.refetchQueries({ queryKey: ['user', userId] }) .then(() => { // Después refetch los dependientes return Promise.all([ queryClient.refetchQueries({ queryKey: ['posts', 'user', userId] }), queryClient.refetchQueries({ queryKey: ['stats', 'user', userId] }) ]); }); };
return ( <div> <button onClick={refetchUserData}> 🔄 Refetch Todo el Usuario </button> {/* Render user data */} </div> );}
Throttled Refetching
Section titled “Throttled Refetching”function ThrottledRefetching() { const [lastRefetch, setLastRefetch] = useState(0); const THROTTLE_MS = 5000; // 5 segundos
const { data, refetch } = useQuery({ queryKey: ['throttled-data'], queryFn: fetchData, refetchOnWindowFocus: false, // Manejamos manualmente });
// Refetch throttleado const throttledRefetch = useCallback(() => { const now = Date.now(); if (now - lastRefetch < THROTTLE_MS) { console.log('Refetch throttled'); return; }
setLastRefetch(now); refetch(); }, [lastRefetch, refetch]);
// Refetch al enfocar con throttle useEffect(() => { const handleFocus = () => throttledRefetch(); window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [throttledRefetch]);
return ( <div> <button onClick={throttledRefetch}> Refetch (máximo cada 5s) </button> <div> Último refetch: {new Date(lastRefetch).toLocaleTimeString()} </div> </div> );}
✅ Mejores Prácticas para Background Refetching
Section titled “✅ Mejores Prácticas para Background Refetching”- Configura staleTime apropiadamente: Determina cuándo los datos necesitan actualizarse
- Usa refetchInterval con cuidado: Para datos que realmente cambian frecuentemente
- Considera refetchIntervalInBackground: Basado en si necesitas datos actualizados sin foco
- Implementa refetch condicional: No todas las queries necesitan la misma estrategia
- Usa visibility-based refetching: Para componentes pesados que solo se actualizan cuando son visibles
- Throttle refetches frecuentes: Para evitar spam de peticiones
- Maneja estados de loading: Usa
isFetching
para mostrar indicadores sutiles - Considera el costo del servidor: Refetch inteligente basado en importancia de datos
- Implementa refetch en cascada: Para datos dependientes que deben actualizarse juntos
- Monitorea performance: Refetching excesivo puede impactar rendimiento
¡Excelente! Ahora dominas todas las estrategias de background refetching. Continuemos con ejemplos prácticos del mundo real.
Próximo paso: CRUD con TanStack Query