Working with Cloud Firestore really feels amazing, realtime, scalability, easy to use, nice SDKs !
Then you stumble on concurrency, and it hurts. Cuncurrency has always been a big issue in many online apps, and there's a lot of ways to handle it, today i'm going to present to you one of the easiest ways I've ever seen, using a boolean that's quite hard to find when you just google "firestore concurrency" stuff.
First of all, the problem with concurrency
Let's imagine this flow of actions, with two users: A and B, both observing the same document ({ "name": "bar", "size": 10}
) and editing it.
- A edits field "name" to set it to "foo".
- B edits field "size" to set it to 25. These actions happen with less than a second between each other.
Because of the Firestore restriction that limits write ops to 1/s/doc (a given document can only be edited once per second), this is what B will see, in sequence:
-
{ "name": "bar", "size": 10}
// First read -
{ "name": "bar", "size": 25}
// Optimistic local write -
{ "name": "foo", "size": 10}
// Result from A's edit -
{ "name": "foo", "size": 25}
// Server result from B's edit
As you can see here, the user will see the data flicker from previous to new state, and this is because we don't filter on "wait until all operations are done".
The hasPendingWrites boolean
Firebase Web JS SDK has a nice tool to handle this kind of issues: hasPendingWrites
(see more details in docs). What this does it tell us if the snapshot we're getting can be considered as stable or not for current client, as it won't be stable if it has pending writes.
This is even available in other Firebase SDKs but we're not going to cover them here.
How to use it
Here I'm going to give an example with @angular/fire
's approach, using rxjs, but you can use it from any JS SDK library, or the SDK itself if you want to, as it's a SDK-level property.
With RxJS, you can simply use the filter
operator on your snapshot listener:
docSnapshots(doc(firestore, 'exampleCollection', 'exampleKey'))
.pipe(
filter(snap => !snap.metadata.hasPendingWrites),
map(snap => snap.data())
)
Et voilà ! You now have a document data observable that will only emit data once all your write operations have been resolved 🎉