PatternsData Fetching Strategies
Polling and Real-Time Sync
Keep data fresh with polling, WebSockets, and Server-Sent Events
Keep your app's data synchronized with the server. This guide covers polling and real-time update patterns.
🎯 When to Use Each
Polling: Simple, works everywhere, predictable load
WebSockets: Bi-directional, instant updates, complex
SSE: Server → Client only, simpler than WebSockets
Long Polling: Fallback for old browsers⏱️ Pattern 1: Simple Polling
Fetch data at regular intervals.
import { useQuery } from '@tanstack/react-query';
function NotificationBell() {
const { data: notifications } = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 30000, // Poll every 30 seconds
});
const unreadCount = notifications?.filter(n => !n.read).length || 0;
return (
<button>
🔔 {unreadCount > 0 && <span>{unreadCount}</span>}
</button>
);
}With Conditional Polling
function OrderStatus({ orderId }: { orderId: string }) {
const { data: order } = useQuery({
queryKey: ['order', orderId],
queryFn: () => fetchOrder(orderId),
refetchInterval: (data) => {
// Stop polling when order is complete
if (data?.status === 'delivered') {
return false;
}
// Poll more frequently for pending orders
if (data?.status === 'pending') {
return 5000; // 5 seconds
}
// Poll less frequently otherwise
return 30000; // 30 seconds
},
});
return (
<div>
<h3>Order Status: {order?.status}</h3>
<ProgressBar status={order?.status} />
</div>
);
}Pros:
- ✅ Simple to implement
- ✅ Works everywhere
- ✅ Predictable server load
Cons:
- ⚠️ Wastes bandwidth (polling when no changes)
- ⚠️ Delayed updates (up to interval time)
- ⚠️ Battery drain on mobile
🚀 Pattern 2: Smart Polling (Adaptive Interval)
Adjust polling frequency based on activity.
class AdaptivePoller {
private interval: number = 30000; // Start with 30s
private minInterval: number = 5000; // Min 5s
private maxInterval: number = 60000; // Max 60s
private consecutiveNoChanges: number = 0;
getInterval(): number {
return this.interval;
}
onDataChanged() {
// Data changed, poll more frequently
this.interval = Math.max(this.minInterval, this.interval / 2);
this.consecutiveNoChanges = 0;
}
onDataUnchanged() {
// No changes, slow down polling
this.consecutiveNoChanges++;
if (this.consecutiveNoChanges >= 3) {
this.interval = Math.min(this.maxInterval, this.interval * 1.5);
}
}
reset() {
this.interval = 30000;
this.consecutiveNoChanges = 0;
}
}
// Usage
function useAdaptivePolling(queryKey: string[], queryFn: () => Promise<any>) {
const poller = useRef(new AdaptivePoller());
const [prevData, setPrevData] = useState<any>(null);
const { data } = useQuery({
queryKey,
queryFn,
refetchInterval: poller.current.getInterval(),
});
useEffect(() => {
if (data) {
if (JSON.stringify(data) !== JSON.stringify(prevData)) {
poller.current.onDataChanged();
} else {
poller.current.onDataUnchanged();
}
setPrevData(data);
}
}, [data, prevData]);
return data;
}
// Example usage
function NotificationBell() {
const notifications = useAdaptivePolling(
['notifications'],
fetchNotifications
);
return <div>🔔 {notifications?.length}</div>;
}🌐 Pattern 3: WebSocket Real-Time Updates
Bi-directional, instant updates.
// WebSocket hook
function useWebSocket(url: string) {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const ws = new WebSocket(url);
ws.onopen = () => {
console.log('WebSocket connected');
setIsConnected(true);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setIsConnected(false);
// Reconnect after 5 seconds
setTimeout(() => {
setSocket(new WebSocket(url));
}, 5000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
setSocket(ws);
return () => {
ws.close();
};
}, [url]);
return { socket, isConnected };
}
// Real-time notifications
function NotificationBell() {
const { socket, isConnected } = useWebSocket('wss://api.example.com/notifications');
const queryClient = useQueryClient();
useEffect(() => {
if (!socket) return;
socket.onmessage = (event) => {
const notification = JSON.parse(event.data);
// Update query cache
queryClient.setQueryData<Notification[]>(['notifications'], (old = []) => [
notification,
...old
]);
// Show toast
toast.info(notification.message);
};
}, [socket, queryClient]);
const { data: notifications = [] } = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications
});
return (
<div>
🔔 {notifications.length}
{!isConnected && <span>⚠️ Reconnecting...</span>}
</div>
);
}With Reconnection Logic
class WebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 1000;
constructor(url: string) {
this.url = url;
this.connect();
}
private connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected');
this.reconnectAttempts = 0;
this.reconnectDelay = 1000;
};
this.ws.onclose = () => {
console.log('Disconnected');
this.reconnect();
};
this.ws.onerror = (error) => {
console.error('Error:', error);
};
}
private reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
console.log(
`Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`
);
setTimeout(() => {
this.connect();
this.reconnectDelay *= 2; // Exponential backoff
}, this.reconnectDelay);
}
send(data: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
onMessage(callback: (data: any) => void) {
if (this.ws) {
this.ws.onmessage = (event) => {
callback(JSON.parse(event.data));
};
}
}
close() {
this.ws?.close();
}
}Pros:
- ✅ Instant updates (< 100ms)
- ✅ Bi-directional communication
- ✅ Efficient for frequent updates
Cons:
- ⚠️ More complex setup
- ⚠️ Requires WebSocket server
- ⚠️ Connection management needed
📡 Pattern 4: Server-Sent Events (SSE)
Server → Client only, simpler than WebSockets.
function useServerSentEvents(url: string) {
const [data, setData] = useState<any>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onopen = () => {
console.log('SSE connected');
setIsConnected(true);
};
eventSource.onmessage = (event) => {
const newData = JSON.parse(event.data);
setData(newData);
};
eventSource.onerror = () => {
console.error('SSE error');
setIsConnected(false);
eventSource.close();
// Reconnect after 5 seconds
setTimeout(() => {
// EventSource will auto-reconnect
}, 5000);
};
return () => {
eventSource.close();
};
}, [url]);
return { data, isConnected };
}
// Usage - Live order updates
function OrderTracking({ orderId }: { orderId: string }) {
const { data: orderUpdate, isConnected } = useServerSentEvents(
`/api/orders/${orderId}/updates`
);
return (
<div>
<h3>Order Status</h3>
{!isConnected && <span>Reconnecting...</span>}
{orderUpdate && (
<div>
<p>Status: {orderUpdate.status}</p>
<p>Location: {orderUpdate.location}</p>
<p>ETA: {orderUpdate.eta}</p>
</div>
)}
</div>
);
}Backend (Node.js)
app.get('/api/orders/:id/updates', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// Send update every 5 seconds
const interval = setInterval(() => {
const update = getOrderUpdate(req.params.id);
res.write(`data: ${JSON.stringify(update)}\n\n`);
}, 5000);
// Cleanup on disconnect
req.on('close', () => {
clearInterval(interval);
res.end();
});
});Pros:
- ✅ Simpler than WebSockets
- ✅ Auto-reconnect built-in
- ✅ Works with HTTP/2
Cons:
- ⚠️ Server → Client only
- ⚠️ Less browser support than WebSockets
🔄 Pattern 5: Long Polling
Fallback for old browsers.
async function longPoll(url: string, callback: (data: any) => void) {
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
if (response.ok) {
const data = await response.json();
callback(data);
}
// Immediately start next long poll
longPoll(url, callback);
} catch (error) {
console.error('Long poll error:', error);
// Retry after delay
setTimeout(() => {
longPoll(url, callback);
}, 5000);
}
}
// Usage
function NotificationBell() {
const [notifications, setNotifications] = useState<Notification[]>([]);
useEffect(() => {
longPoll('/api/notifications/poll', (newNotification) => {
setNotifications(prev => [newNotification, ...prev]);
});
}, []);
return <div>🔔 {notifications.length}</div>;
}Backend (Node.js)
app.get('/api/notifications/poll', async (req, res) => {
const userId = req.user.id;
// Wait for new notification (max 30 seconds)
const timeout = setTimeout(() => {
res.json({ type: 'timeout' });
}, 30000);
// Listen for new notification
const listener = (notification) => {
clearTimeout(timeout);
res.json(notification);
};
notificationEmitter.once(`notification:${userId}`, listener);
// Cleanup on disconnect
req.on('close', () => {
clearTimeout(timeout);
notificationEmitter.off(`notification:${userId}`, listener);
});
});🎯 Pattern 6: Hybrid Approach
Combine polling with WebSockets.
function useRealtimeData<T>(
queryKey: string[],
queryFn: () => Promise<T>,
websocketUrl?: string
) {
const queryClient = useQueryClient();
// Initial fetch + polling fallback
const { data } = useQuery({
queryKey,
queryFn,
refetchInterval: websocketUrl ? false : 30000, // Poll only if no WS
});
// WebSocket for real-time updates
useEffect(() => {
if (!websocketUrl) return;
const ws = new WebSocket(websocketUrl);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
// Update cache with real-time data
queryClient.setQueryData<T>(queryKey, (old) => ({
...old,
...update
}));
};
ws.onerror = () => {
// Fallback to polling on WebSocket error
queryClient.invalidateQueries({ queryKey });
};
return () => ws.close();
}, [websocketUrl, queryKey, queryClient]);
return data;
}
// Usage
function Dashboard() {
const stats = useRealtimeData(
['stats'],
fetchStats,
'wss://api.example.com/stats'
);
return <div>Users online: {stats?.usersOnline}</div>;
}📊 Comparison Table
| Method | Latency | Complexity | Server Load | Use Case |
|---|---|---|---|---|
| Simple Polling | 5-30s | ⭐ | High | Notifications |
| Adaptive Polling | 5-60s | ⭐⭐ | Medium | Dynamic data |
| WebSockets | < 100ms | ⭐⭐⭐⭐ | Low | Chat, gaming |
| SSE | < 100ms | ⭐⭐ | Low | Live feeds |
| Long Polling | < 1s | ⭐⭐⭐ | High | Fallback |
| Hybrid | < 100ms | ⭐⭐⭐⭐ | Low | Production apps |
🏢 Real-World Examples
Slack
// WebSockets for messages
// Polling fallback
// Reconnection logicGmail
// Long polling for new emails
// SSE for real-time presence
// Smart polling when idle// WebSockets for timeline updates
// Polling for notifications
// Adaptive intervals📚 Key Takeaways
- Start with polling - Simple and works
- Use WebSockets for real-time (< 1s updates)
- SSE for server → client only
- Adaptive polling to reduce waste
- Always have reconnection logic
- Hybrid approach for production
- Monitor battery impact on mobile
Choose based on latency requirements and complexity tolerance.