Skip to content

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.

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 loading
const { 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

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>
);
}

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>
);
}

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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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”
  1. Configura staleTime apropiadamente: Determina cuándo los datos necesitan actualizarse
  2. Usa refetchInterval con cuidado: Para datos que realmente cambian frecuentemente
  3. Considera refetchIntervalInBackground: Basado en si necesitas datos actualizados sin foco
  4. Implementa refetch condicional: No todas las queries necesitan la misma estrategia
  5. Usa visibility-based refetching: Para componentes pesados que solo se actualizan cuando son visibles
  6. Throttle refetches frecuentes: Para evitar spam de peticiones
  7. Maneja estados de loading: Usa isFetching para mostrar indicadores sutiles
  8. Considera el costo del servidor: Refetch inteligente basado en importancia de datos
  9. Implementa refetch en cascada: Para datos dependientes que deben actualizarse juntos
  10. 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