name: real-time-features description: Use when implementing real-time updates, WebSocket connections, live data synchronization, or Supabase Realtime subscriptions - focuses on real-time data patterns tags: domain: feature-development tools: [websocket, supabase-realtime, socket.io] symptoms: [data not updating, subscription not working, websocket disconnected, stale data] keywords: [real-time, WebSocket, live updates, subscriptions, Supabase Realtime, Socket.io] priority: medium prerequisites: [react-project]
Real-Time Features
When to Use
- Implementing live data updates
- Building collaborative features (live cursors, presence)
- Setting up WebSocket connections
- Using Supabase Realtime subscriptions
- Synchronizing data across clients in real-time
Process
1. Setup Supabase Realtime (Recommended)
☐ Enable Realtime in Supabase dashboard ☐ Configure table-level replication ☐ Set Row Level Security (RLS) policies ☐ Install Supabase client (if not already)
// lib/supabase.ts - Already configured from auth setup
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
);
2. Subscribe to Database Changes
☐ Create subscription to table ☐ Handle INSERT, UPDATE, DELETE events ☐ Update local state on changes ☐ Cleanup subscription on unmount
// hooks/useRealtimeMessages.ts
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
interface Message {
id: string;
content: string;
created_at: string;
user_id: string;
}
export function useRealtimeMessages(channelId: string) {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
// Fetch initial messages
const fetchMessages = async () => {
const { data } = await supabase
.from('messages')
.select('*')
.eq('channel_id', channelId)
.order('created_at', { ascending: true });
if (data) setMessages(data);
};
fetchMessages();
// Subscribe to new messages
const subscription = supabase
.channel(`messages:${channelId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
(payload) => {
setMessages((prev) => [...prev, payload.new as Message]);
}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
(payload) => {
setMessages((prev) =>
prev.map((msg) =>
msg.id === payload.new.id ? (payload.new as Message) : msg
)
);
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'messages',
filter: `channel_id=eq.${channelId}`,
},
(payload) => {
setMessages((prev) =>
prev.filter((msg) => msg.id !== payload.old.id)
);
}
)
.subscribe();
// Cleanup on unmount
return () => {
subscription.unsubscribe();
};
}, [channelId]);
return messages;
}
3. Implement Presence (Who's Online)
☐ Create presence channel ☐ Track user join/leave events ☐ Show online users list ☐ Handle presence state updates
// hooks/usePresence.ts
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
interface PresenceUser {
userId: string;
username: string;
online_at: string;
}
export function usePresence(channelId: string, currentUser: { id: string; username: string }) {
const [onlineUsers, setOnlineUsers] = useState<PresenceUser[]>([]);
useEffect(() => {
const channel = supabase.channel(`presence:${channelId}`);
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
const users = Object.values(state).flat() as PresenceUser[];
setOnlineUsers(users);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track current user's presence
await channel.track({
userId: currentUser.id,
username: currentUser.username,
online_at: new Date().toISOString(),
});
}
});
return () => {
channel.unsubscribe();
};
}, [channelId, currentUser.id, currentUser.username]);
return onlineUsers;
}
4. Implement Broadcast (Client-to-Client)
☐ Create broadcast channel ☐ Send events to other clients ☐ Listen for broadcast events ☐ Handle typing indicators, live cursors
// hooks/useTypingIndicator.ts
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
export function useTypingIndicator(channelId: string, userId: string) {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
useEffect(() => {
const channel = supabase.channel(`typing:${channelId}`);
channel
.on('broadcast', { event: 'typing' }, ({ payload }) => {
if (payload.userId !== userId) {
setTypingUsers((prev) => {
if (!prev.includes(payload.userId)) {
return [...prev, payload.userId];
}
return prev;
});
// Remove after 3 seconds
setTimeout(() => {
setTypingUsers((prev) => prev.filter((id) => id !== payload.userId));
}, 3000);
}
})
.subscribe();
return () => {
channel.unsubscribe();
};
}, [channelId, userId]);
const broadcastTyping = () => {
const channel = supabase.channel(`typing:${channelId}`);
channel.send({
type: 'broadcast',
event: 'typing',
payload: { userId },
});
};
return { typingUsers, broadcastTyping };
}
5. Handle Connection State
☐ Track connection status ☐ Show reconnecting indicator ☐ Handle offline state gracefully ☐ Queue updates during disconnection
// hooks/useRealtimeConnection.ts
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
export function useRealtimeConnection() {
const [status, setStatus] = useState<'connected' | 'disconnected' | 'reconnecting'>('disconnected');
useEffect(() => {
const channel = supabase.channel('connection-status');
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
setStatus('connected');
} else if (status === 'CHANNEL_ERROR') {
setStatus('disconnected');
} else if (status === 'TIMED_OUT') {
setStatus('reconnecting');
}
});
return () => {
channel.unsubscribe();
};
}, []);
return status;
}
// components/ConnectionStatus.tsx
import { useRealtimeConnection } from '../hooks/useRealtimeConnection';
export function ConnectionStatus() {
const status = useRealtimeConnection();
if (status === 'connected') return null;
return (
<div className="fixed top-0 left-0 right-0 bg-yellow-500 text-white p-2 text-center">
{status === 'disconnected' && 'Disconnected from server'}
{status === 'reconnecting' && 'Reconnecting...'}
</div>
);
}
6. Optimize Real-Time Performance
☐ Use channel multiplexing (one channel per feature) ☐ Filter subscriptions server-side (where possible) ☐ Debounce broadcast events (typing indicators) ☐ Unsubscribe on unmount (prevent memory leaks)
// Debounce typing broadcasts
import { useEffect, useRef } from 'react';
function useDebounce(callback: () => void, delay: number) {
const timeoutRef = useRef<number>();
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(callback, delay);
};
}
export function ChatInput({ channelId, userId }: { channelId: string; userId: string }) {
const { broadcastTyping } = useTypingIndicator(channelId, userId);
const debouncedTyping = useDebounce(broadcastTyping, 300);
return (
<input
onChange={(e) => {
debouncedTyping();
// ... handle input change
}}
/>
);
}
7. Alternative: WebSocket with Socket.io
☐ Setup Socket.io server (backend required) ☐ Create Socket.io client connection ☐ Listen for events from server ☐ Emit events to server
// lib/socket.ts
import { io } from 'socket.io-client';
export const socket = io(import.meta.env.VITE_SOCKET_URL, {
autoConnect: false,
});
// hooks/useSocket.ts
import { useEffect, useState } from 'react';
import { socket } from '../lib/socket';
export function useSocket() {
const [connected, setConnected] = useState(socket.connected);
useEffect(() => {
socket.connect();
socket.on('connect', () => setConnected(true));
socket.on('disconnect', () => setConnected(false));
return () => {
socket.disconnect();
};
}, []);
return { socket, connected };
}
8. Verify Real-Time Features Work
☐ Test data updates appear immediately ☐ Test presence shows online users correctly ☐ Test broadcast events (typing indicators) ☐ Test reconnection after disconnection ☐ Test with multiple clients/tabs open ☐ Verify subscriptions clean up on unmount
Common Pitfalls
Not cleaning up subscriptions:
// ❌ Wrong - subscription never cleaned up
useEffect(() => {
supabase.channel('messages').subscribe();
}, []);
// ✅ Right - cleanup on unmount
useEffect(() => {
const channel = supabase.channel('messages').subscribe();
return () => channel.unsubscribe();
}, []);
Creating too many channels:
// ❌ Wrong - new channel for every message
messages.forEach(msg => {
supabase.channel(`message-${msg.id}`).subscribe();
});
// ✅ Right - one channel with filtering
supabase.channel('all-messages')
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, handler)
.subscribe();
Red Flags
Never:
- Skip subscription cleanup (causes memory leaks)
- Create duplicate subscriptions (check if already subscribed)
- Send too many broadcast events (debounce or throttle)
- Expose sensitive data in real-time channels (use RLS)
Always:
- Unsubscribe on component unmount
- Handle connection state (show reconnecting indicator)
- Use Row Level Security (RLS) for Supabase Realtime
- Test with multiple clients to verify real-time sync
- Debounce high-frequency events (typing, mouse movement)
- Handle offline state gracefully
- Monitor real-time connection quota/limits