FrontendAdvanced
14 min readNov 24, 2025

Building a Realtime UI with Firestore Streams

Implement live updates, optimistic UI, and offline-first patterns with Firestore real-time listeners.

R

Rithy Tep

Author

Building a Realtime UI with Firestore Streams

Real-time Listeners

import { onSnapshot, collection } from 'firebase/firestore' function useRealtimeMessages(chatId: string) { const [messages, setMessages] = useState([]) useEffect(() => { const unsubscribe = onSnapshot( collection(db, 'chats', chatId, 'messages'), (snapshot) => { const newMessages = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })) setMessages(newMessages) } ) return () => unsubscribe() }, [chatId]) return messages }

Optimistic Updates

async function sendMessage(text: string) { const tempId = `temp-${Date.now()}` // Optimistic update setMessages(prev => [...prev, { id: tempId, text, sending: true, createdAt: new Date() }]) try { const docRef = await addDoc(collection(db, 'messages'), { text, userId: auth.currentUser.uid, createdAt: serverTimestamp() }) // Replace temp message with real one setMessages(prev => prev.map(msg => msg.id === tempId ? { ...msg, id: docRef.id, sending: false } : msg) ) } catch (error) { // Revert on error setMessages(prev => prev.filter(msg => msg.id !== tempId)) showError('Failed to send message') } }

Offline-First Pattern

import { enableIndexedDbPersistence } from 'firebase/firestore' // Enable offline persistence enableIndexedDbPersistence(db).catch((err) => { if (err.code === 'failed-precondition') { console.warn('Multiple tabs open') } else if (err.code === 'unimplemented') { console.warn('Browser doesn't support offline') } }) // Data syncs automatically when back online

Handling Connection State

import { onDisconnect, ref, set } from 'firebase/database' function usePresence(userId: string) { useEffect(() => { const userStatusRef = ref(rtdb, `/status/${userId}`) onDisconnect(userStatusRef).set({ state: 'offline', lastSeen: Date.now() }) set(userStatusRef, { state: 'online', lastSeen: Date.now() }) }, [userId]) }

Performance Tips

  1. Use limit() to avoid loading too much data
  2. Implement pagination with startAfter()
  3. Cache aggressively with persistence
  4. Show loading skeletons during initial load
#Firestore#Real-time#React#Vue#Offline-First