Skip to content

[🐛] withConverter.fromFirestore + snapshot.data({ serverTimestamps: 'estimate' }) returns null for pending serverTimestamp() fields #9016

@harelw

Description

@harelw

withConverter.fromFirestore + snapshot.data({ serverTimestamps: 'estimate' }) returns null for pending serverTimestamp() fields

Issue

When a FirestoreDataConverter.fromFirestore callback calls snapshot.data({ serverTimestamps: 'estimate' }), fields written with serverTimestamp() come back as null for any emission where metadata.hasPendingWrites === true — even though 'estimate' is supposed to substitute pending sentinels with a client-clock-estimated Timestamp.

Repro: https://github.com/harelw/rnfb-timestamp-issue-repro

Versions

  • @react-native-firebase/app: 24.0.0
  • @react-native-firebase/firestore: 24.0.0
  • expo: ~55.0.23
  • react-native: 0.83.6
  • iOS simulator (iPhone 17 Pro, iOS 26.x); same shape observed on Android emulator.
  • Reproduced against the Firestore emulator (firebase-tools 14.27.0), so no production project state involved.

Setup

The repo is wired for the Firestore emulator. After pnpm install && pnpm prebuild:

pnpm emulators   # terminal 1
pnpm ios         # terminal 2

A real GoogleService-Info.plist is required just to clear Firebase iOS SDK's startup validation; all Firestore traffic from the running app goes to localhost:8080.

What the test does

const ref = collection(db, 'rnfb_timestamp_repro').withConverter(sampleConverter);
const docRef = doc(ref);  // pre-generate client-side id

await new Promise<void>((resolve, reject) => {
  const unsub = onSnapshot(docRef, { includeMetadataChanges: true }, (snap) => {
    // snap.data() runs through fromFirestore which calls
    // snap.data({ serverTimestamps: 'estimate' })
    if (snap.exists() && !snap.metadata.hasPendingWrites) {
      unsub();
      resolve();
    }
  }, reject);

  // Fire-and-forget — observe local-write echo, then server-acked frame
  setDoc(docRef, {}).catch(reject);
});

sampleConverter's toFirestore writes both createdAt and updatedAt as serverTimestamp(). Its fromFirestore reads snap.data({ serverTimestamps: 'estimate' }).

Expected

Per SnapshotOptions.serverTimestamps, 'estimate' should substitute pending serverTimestamps with the client clock. So every emission — including the hasPendingWrites=true ones — should carry Timestamp(<estimated>) for both fields.

Observed

Emissions: 3

[0] exists=true  hasPendingWrites=true   fromCache=true
     createdAt: null
     updatedAt: null

[1] exists=true  hasPendingWrites=true   fromCache=false
     createdAt: null
     updatedAt: null

[2] exists=true  hasPendingWrites=false  fromCache=false
     createdAt: Timestamp(2026-05-11T19:22:38.776Z)
     updatedAt: Timestamp(2026-05-11T19:22:38.776Z)

Both pending emissions return null despite 'estimate' being passed to data(). The settled emission resolves correctly. Two pending emissions, not just one — the bug persists even after the snapshot has been refreshed from network (fromCache=false), as long as hasPendingWrites=true.

Why this matters

Any consumer that validates the converter output (Zod, runtime type assertion, downstream .toMillis()) crashes on the first emission of a freshly-written document. In our app, this surfaced as account-setup failure on first login after sign-up: an immediate read of a doc written with serverTimestamp() failed schema validation because both timestamp fields were null.

The documented 'estimate' workaround is supposed to prevent this exact scenario but doesn't, at least under withConverter.

Current workaround

Inside fromFirestore, check snapshot.metadata.hasPendingWrites and substitute the current Date for known-server-set fields when true. This duplicates what 'estimate' is supposed to do natively, but it's the only way to keep a strict schema in front of the converter today.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions