Appearance
useRealtime
What this solves
Realtime data updates are common in admin dashboards:
- Live visitor counts
- Order notifications as they happen
- Status changes pushed from the server
- Activity feeds and alerts
Ad-hoc implementations often have:
- Inconsistent connection management across screens
- No fallback when WebSocket isn't available
- Duplicated reconnection/retry logic
- Complex state synchronization between WS and polling
useRealtimeChannel, useRealtimeMode, and related hooks provide a consistent infrastructure for realtime data with automatic mode switching.
When to use it
Use these hooks when you need:
- Push-based updates from a WebSocket server
- Automatic fallback to polling when WebSocket isn't available or desired
- Connection state awareness (connected, disconnected, reconnecting)
- Consistent patterns across different realtime features
When NOT to use it
- For one-time data fetching (use React Query directly)
- When you don't have a WebSocket endpoint available
- For simple polling without WebSocket option (use
useQuerywithrefetchInterval)
Core primitives
| Hook | Purpose |
|---|---|
useRealtimeChannel | Subscribe to a specific channel/topic |
useRealtimeMode | Global mode switching (WebSocket vs Polling) |
useRealtimeConnection | Low-level connection management |
useRealtimeCounter | Pre-built hook for counter values with trend |
useRealtimeList | Pre-built hook for lists with new-item highlighting |
Basic usage
useRealtimeChannel — Subscribe to a channel
tsx
import { useRealtimeChannel } from '@/shared/infrastructure/realtime'
interface OrderNotification {
id: string
amount: number
timestamp: string
}
export function OrderFeed() {
const { isConnected, lastMessage } = useRealtimeChannel<OrderNotification>({
path: '/ws/orders/realtime',
channel: 'new-order',
})
return (
<div>
<span>{isConnected ? '🟢 Live' : '🟡 Connecting...'}</span>
{lastMessage && (
<p>Latest order: ${lastMessage.data.amount}</p>
)}
</div>
)
}useRealtimeMode — Switch between modes
tsx
import { useRealtimeMode } from '@/shared/infrastructure/realtime'
export function RealtimeSettings() {
const { mode, setMode } = useRealtimeMode()
return (
<select value={mode} onChange={(e) => setMode(e.target.value as 'websocket' | 'polling')}>
<option value="websocket">WebSocket (Real-time)</option>
<option value="polling">Polling (Fallback)</option>
</select>
)
}useRealtimeCounter — Counter with trend
tsx
import { useRealtimeCounter } from '@/shared/infrastructure/realtime'
export function ActiveUsers() {
const { count, trend, isConnected } = useRealtimeCounter({
path: '/ws/metrics/realtime',
channel: 'active-users',
countField: 'count', // Field in the message containing the count
})
return (
<div>
<h3>{count ?? '—'} active users</h3>
<span>{trend === 'up' ? '📈' : trend === 'down' ? '📉' : '➡️'}</span>
</div>
)
}useRealtimeList — List with new-item highlighting
tsx
import { useRealtimeList } from '@/shared/infrastructure/realtime'
interface Notification {
id: string
message: string
}
export function NotificationFeed() {
const { items, newItemIds, isConnected } = useRealtimeList<Notification>({
path: '/ws/notifications',
channel: 'notification',
maxItems: 50,
extractItem: (msg) => msg.data as Notification,
getItemId: (n) => n.id,
})
return (
<ul>
{items.map((item) => (
<li
key={item.id}
className={newItemIds.has(item.id) ? 'highlight' : ''}
>
{item.message}
</li>
))}
</ul>
)
}Building domain-specific hooks
The core primitives are designed to be composed into domain-specific hooks. Here's the recommended pattern:
tsx
import { useRealtimeChannel, useRealtimeMode } from '@/shared/infrastructure/realtime'
import { usePollingHook } from './polling-hooks'
interface UseLiveDataOptions {
pollInterval?: number
}
export function useLiveData(options: UseLiveDataOptions = {}) {
const { pollInterval = 5000 } = options
const { mode } = useRealtimeMode()
// State for domain-specific logic
const [data, setData] = useState<MyData | null>(null)
// WebSocket: Use shared primitive
const { isConnected, lastMessage } = useRealtimeChannel<MyData>({
path: '/ws/my-app/realtime',
channel: 'my-channel',
})
// Process WebSocket messages
useEffect(() => {
if (mode !== 'websocket' || !lastMessage?.data) return
setData(lastMessage.data)
}, [mode, lastMessage])
// Polling: Fallback
const pollingResult = usePollingHook({
pollInterval,
enabled: mode === 'polling',
})
useEffect(() => {
if (mode !== 'polling') return
setData(pollingResult.data)
}, [mode, pollingResult.data])
return {
data,
mode,
isConnected: mode === 'websocket' ? isConnected : true,
}
}Server-side message format
The hooks expect messages in this format:
json
{
"type": "channel-name",
"channel": "channel-name",
"data": { /* your payload */ },
"timestamp": "2024-01-15T10:30:00Z"
}The channel field is used for routing messages to the correct subscribers.
Mode persistence
useRealtimeMode automatically persists the selected mode to localStorage. Users who prefer polling (or are in environments where WebSocket is blocked) will have their preference remembered.
Related
- useRealtime API Reference — Function signatures and type definitions
- Realtime Data Pattern — Building unified hooks with fallback
- Realtime Components — UI components for displaying realtime data