angularfire: Second subscribe on Observable from snapshotChanges() does not emit the current snapshot
Version info
Angular: 5.1.1
Firebase: 5.0.0-rc.4
AngularFire: 5.0.0-rc.5
How to reproduce these conditions
This Stackblitz demonstrates the problem. https://stackblitz.com/edit/angular-keww6x
Steps to set up and reproduce
Create an Observable
by calling valueChanges()
or snapshotChanges()
on a ref in the Firebase Database (eg. const data$ = db.object('data').valueChanges()
).
Then asynchronously subscribe twice to that single Observable.
Sample data and security rules
The same database as used in the Stackblitz can be used. Or any other database, as long as there is data in it.
Expected behavior
When subscribing to the Observable, I expect to always get the current value first and all the changes after that.
Actual behavior
The first subscription gets the current value and any changes. The second subscription does not get the first value, only the subsequent changes.
If all the subscriptions are unsubscribed, however, the next subscription gets the initial value again.
Workaround
The current workaround in my code is to surround all the valueChanges()
and snapshotChanges()
calls with an Observable.defer()
.
Suspected culprit
The share()
operator in fromRef
.
This operator shares the subscription with all of its subscribers, but will not emit the initial value a second time.
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 6
- Comments: 19 (3 by maintainers)
This is a tricky. It’s a hot vs. cold observable situation.
All AngularFire observables are hot, meaning they values are emitted live. A second subscribe will not emit the “original” value. This is the intended design.
Why are AngularFire observables hot? A Firebase/Firestore reference is created outside the observable, which by default makes it hot. I’ll be citing @BenLesh’s excellent article to explain.
Below is an example of a cold observable:
This would create a new reference for each subscribe. Which is not ideal. Instead we create the reference outside of the observable, which makes the observable hot.
The kink in this situation is that methods like
.on('child_added')
oronSnapshot()
work in a cold like manner. Meaning each time you call them they will replay the same results in the callback.Ideally, you wouldn’t run into this situation because
subscribe()
should be called just once. However, I am willing to make a change to have these observables act in a cold like manner to match the Firebase SDK.I’m working on a few infrastructure changes with our typings, but after that I will get this change released.
I’m using the operator shareReplay on snapshotChanges() to be able to subscribe several times.
Same issue here. Following for feedback.
@jamesdaniels It would be fantastic to see a change that does make this issue less confusing. Requiring .subscribe() to be called once only in an angular service seems to contradict the prevailing angular teachings that encourage using the async pipe (eg: {myObservable$ | async}) in component views. (Unless I have misunderstood something).
I wish I could be more helpful, but I’m still a newbie trying to learn this stuff. Thanks for your hard work.
So glad to have finally found an explanation of what is going on. Any updates on your progress with changing to a cold observable emission @davideast .
For now though what is the recommended workaround as I am very keen to move over to as much async await with take(1) that makes sense.
Any chance you could elaborate on
I made a simple fn that works as I expect it:
This ensures that any new subscriptions on the returned observable will reuse one observable and also replay the most recent snapshot change.
The drawback is that this is running outside the Angular zone, so you have to make sure you emit your results in a zone if that’s important.