Building Offline-First Collaboration with Firestore Listeners
Learn how Firestore's offline persistence combined with real-time listeners lets your app stay responsive even when the network drops, and how to detect when data is locally cached versus server-confirmed.
17 Jul 2025, 11:28 UTC

The Problem: Network Fragility in Collaborative Apps
When users edit a shared note, they expect instant feedback. If the device loses connectivity, a naive implementation that waits for server acknowledgment will freeze the UI, making the app feel broken.
How Firestore Offline Persistence Works
Firebase SDK can persist a local copy of Firestore data on disk. When enabled, reads are served from this cache while the client is offline, and writes are queued locally until the network returns.
To turn it on, call enableIndexedDbPersistence() (web) or setPersistenceEnabled(true) (mobile) after initializing the Firebase app.
Implementing a Listener that Knows Its Source
Firestore snapshot metadata includes a source property that tells you whether the data came from the cache, the server, or is a mix. Combined with hasPendingWrites, you can show the right UI state.
Code Example
// Run this after firebase.initializeApp({...}) in your web app.
// Required permission: the user must have read access to the collection.
import { enableIndexedDbPersistence, doc, onSnapshot } from 'firebase/firestore';
import { db } from './firebase-config';
// Enable offline persistence (call once).
enableIndexedDbPersistence(db)
.catch((err) => {
if (err.code === 'failed-precondition') {
console.warn('Persistence can only be enabled in one tab.');
} else if (err.code === 'unimplemented') {
console.warn('The current browser does not support persistence.');
}
});
const noteRef = doc(db, 'notes', 'note-xyz');
const unsubscribe = onSnapshot(noteRef, (snapshot) => {
if (!snapshot.exists()) return;
const data = snapshot.data();
const fromCache = snapshot.metadata.fromCache;
const hasPending = snapshot.metadata.hasPendingWrites;
// UI logic
updateNoteUI(data);
if (fromCache) {
showIndicator('Offline – showing cached data');
} else {
hideIndicator();
}
if (hasPending) {
showIndicator('Saving…');
}
}, (error) => {
console.error('Listener error:', error);
});
// Remember to unsubscribe when the component unmounts to avoid leaks.
// Example: useEffect cleanup in React or window.addEventListener('beforeunload', unsubscribe);
Trade-offs and Limitations
- Enabling persistence adds a small amount of local storage usage and slightly increases startup time.
- Every active listener still incurs a document read for each change; offline reads from cache do not count, but once the device reconnects, the sync generates reads.
- If many clients listen to the same document, the combined read cost can grow; consider limiting listener scope to specific documents or using collection queries with limits.
Actionable Closing
To verify the setup, open two browser tabs, enable offline persistence in both, then disable network in one tab via DevTools. Edit the note in the online tab; you should see the offline tab update instantly from its cache when it reconnects. Use the Network tab to confirm that no reads occur while offline, and that a batch of reads appears after reconnection.
By combining Firestore’s offline persistence with metadata‑aware listeners, you can build collaborative apps that remain responsive even when the network falters, while still being able to distinguish locally cached data from server‑confirmed updates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.