
Jurij Tokarski
Merging Two Firestore Listeners for Cross-Field OR Queries
Firestore can't OR across different field types in a real-time query. Two parallel listeners merged client-side can.
The assignment feature needed a real-time subscription: show tenders where creator_id == userId OR assignee_ids array-contains userId. A cross-field OR.
Firestore's Filter.or() works for same-field conditions. For fundamentally different field types — equality vs. array-contains — composite OR queries don't compose cleanly, and the SDK support varies by version. The alternative is a denormalised collection: fan out writes to a user_tenders subcollection on every state change. That's a write-time tax and more surface area for issues down the line.
The working approach: two separate onSnapshot listeners, results merged client-side into a Map keyed by document ID.
export const firebaseCompanyTendersSubscribeByCreatorOrAssignee = (
companyId,
userId,
callback
) => {
const merged = new Map();
const unsubCreator = onSnapshot(
query(COLLECTION_TENDERS(companyId), where("creator_id", "==", userId)),
(snap) => {
snap.docs.forEach((doc) => merged.set(doc.id, { id: doc.id, ...doc.data() }));
snap.docChanges().forEach(({ type, doc }) => {
if (type === "removed") merged.delete(doc.id);
});
callback(Array.from(merged.values()));
}
);
const unsubAssignee = onSnapshot(
query(COLLECTION_TENDERS(companyId), where("assignee_ids", "array-contains", userId)),
(snap) => {
snap.docs.forEach((doc) => merged.set(doc.id, { id: doc.id, ...doc.data() }));
snap.docChanges().forEach(({ type, doc }) => {
if (type === "removed") merged.delete(doc.id);
});
callback(Array.from(merged.values()));
}
);
return () => {
unsubCreator();
unsubAssignee();
};
};
Deduplication is automatic — same document ID overwrites itself in the Map. Either listener updating triggers a re-merge and emits a fresh array. The cleanup path requires calling both unsubscribes; returning just one leaks the other listener.
No fan-out collection. No schema changes. The subscription logic stays in one place.
Subscribe to the newsletter:
About Jurij Tokarski
Hey 👋 I'm Jurij. I run Varstatt and create software. Usually, I'm deep in the work shipping for clients or building for myself. Sometimes, I share bits I don't want to forget: mostly about software, products and self-employment.