TL;DR: WebSockets and TanStack Query work together through a simple pattern: subscribe to WebSocket events on mount, push updates into the query cache, and let Query handle the reconciliation. This guide covers WebSocket subscriptions that update cached queries, deduplication of real-time and polling data, reconnection strategies, and a custom
useWebSocketQueryhook.
Introduction
Real-time data is a table-stakes feature for modern SaaS applications -- collaborative editing, live dashboards, notification badges, and chat. TanStack Query handles server-state caching elegantly, but WebSockets introduce events that need to bypass the normal request lifecycle.
The key insight: WebSocket messages should update the query cache directly, not trigger refetches. This gives you instant updates that only touch the specific data that changed.
WebSocket Subscription Hook
function useWebSocketSubscription<TData>(
channel: string,
onMessage: (data: TData) => void
) {
useEffect(() => {
const ws = new WebSocket(`wss://api.tanstackship.com/ws?channel=${channel}`)
ws.onmessage = (event) => {
const data = JSON.parse(event.data) as TData
onMessage(data)
}
ws.onclose = () => {
// Reconnect after 3 seconds
setTimeout(() => {
new WebSocket(`wss://api.tanstackship.com/ws?channel=${channel}`)
}, 3000)
}
return () => ws.close()
}, [channel, onMessage])
}
The subscription hook handles the full WebSocket lifecycle — connection, message handling, and automatic reconnection after close. For strategies to persist data during offline periods and sync on reconnect, see TanStack Query offline support.
Integrating with Query Cache
TanStack Query's cache is the central hub for server state. When WebSocket events arrive, they update the cache directly via queryClient.setQueryData, avoiding unnecessary network requests. For advanced patterns like optimistic updates and cache garbage collection, see TanStack Query cache invalidation strategies.
function useRealtimeDashboard() {
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['dashboard', 'metrics'],
queryFn: fetchDashboardMetrics,
refetchInterval: 60_000, // Fallback polling
})
useWebSocketSubscription('dashboard:metrics', (metric) => {
queryClient.setQueryData(['dashboard', 'metrics'], (old: any) => {
if (!old) return old
return {
...old,
activeUsers: metric.activeUsers,
recentSignups: old.recentSignups + (metric.newSignup ? 1 : 0),
}
})
})
return data
}
This pattern keeps the dashboard data fresh via WebSocket events while maintaining a 60-second polling fallback. For caching strategies that complement this real-time approach, see the Edge Caching Strategies Guide.
Real-Time Strategy Comparison
| Strategy | Latency | Server Cost | Complexity | Best For |
|---|---|---|---|---|
| WebSocket + Cache Update | Instant | High | Medium | Live dashboards, chat |
| Server-Sent Events | <1s | Medium | Low | One-way feeds |
| Short Polling (3s) | 3-10s | Low | Minimal | Non-critical updates |
| Long Polling | 1-5s | Medium | Medium | Fallback only |
| TanStack Query refetchInterval | Configurable | Low | Minimal | Background sync |
Conclusion
WebSockets + TanStack Query create a powerful real-time architecture where the cache is the single source of truth. WebSocket events update the cache directly, while TanStack Query handles the fallback polling, retry logic, and cache consistency automatically. For production deployments, pair this with SaaS monitoring and observability to track WebSocket health and cache performance.