--- 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) ```tsx // 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 ```tsx // 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([]); 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 ```tsx // 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([]); 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 ```tsx // hooks/useTypingIndicator.ts import { useEffect, useState } from 'react'; import { supabase } from '../lib/supabase'; export function useTypingIndicator(channelId: string, userId: string) { const [typingUsers, setTypingUsers] = useState([]); 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 ```tsx // 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; } ``` ```tsx // components/ConnectionStatus.tsx import { useRealtimeConnection } from '../hooks/useRealtimeConnection'; export function ConnectionStatus() { const status = useRealtimeConnection(); if (status === 'connected') return null; return (
{status === 'disconnected' && 'Disconnected from server'} {status === 'reconnecting' && 'Reconnecting...'}
); } ``` ### 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) ```tsx // Debounce typing broadcasts import { useEffect, useRef } from 'react'; function useDebounce(callback: () => void, delay: number) { const timeoutRef = useRef(); 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 ( { 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 ```tsx // 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:** ```tsx // ❌ 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:** ```tsx // ❌ 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