SkillByAIOpen interactive version →

Lesson 12 / 25

Realtime Listeners and Offline Persistence

onSnapshot and the local cache.

Live data with a local cache

onSnapshot attaches a listener to a document or query: it fires immediately with current data and again whenever results change, and returns an unsubscribe function you must call when the screen goes away. Local writes trigger listeners at once with metadata.hasPendingWrites set (latency compensation) before the server confirms. Mobile SDKs keep an offline cache by default; on the web, enable a persistent cache with initializeFirestore and persistentLocalCache (API details vary by SDK version, check the docs), so reads work offline and writes are queued until the connection returns. Each change delivered to a listener counts as reads, so scope listeners tightly.

A live list with an offline cache

TypeScript.

import {
  initializeFirestore, persistentLocalCache, persistentMultipleTabManager,
  collection, query, where, orderBy, limit, onSnapshot,
} from "firebase/firestore";

const db = initializeFirestore(app, {
  localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }),
});

const q = query(
  collection(db, "rooms", roomId, "messages"),
  orderBy("createdAt", "desc"),
  limit(50),
);

const unsubscribe = onSnapshot(q, (snap) => {
  const messages = snap.docs.map((d) => ({
    id: d.id,
    ...d.data(),
    pending: d.metadata.hasPendingWrites,
  }));
  render(messages);
}, (err) => console.error("listener failed", err));

// when leaving the screen
unsubscribe();

Always unsubscribe

Forgotten listeners keep connections open and keep billing reads; tie them to component lifecycle hooks.

Quick check: What does onSnapshot return?

  • A promise of the first snapshot only
  • A function that removes the listener
  • The document id
  • A transaction object
Answer

A function that removes the listener — Call it when the view is destroyed.