I have a schema in Firebase that looks like this:
messages/
$groupId/
$messageId/
message: 'Sample Message'
createdBy: 'userID'
createdAt: 1513337977055
Then I have the following queries executed consecutively in my code:
// Get a specific message
ref.child('messages/$groupId/$messageId')
.once('value')
.then(snap => console.log('value', snap.val()))
// Start new message listener
ref.child('messages/$groupId')
.orderByKey()
.limitToLast(1)
.on('child_added', snap => console.log('child_added', snap.val()))
I'm wondering why child_added
gets called twice here, the first one being similar to the value returned by the once('value')
query.
Here's what the console displays:
child_added { message: 'Hello', createdAt: 1513337977055, createdBy: 'userId' }
value { message: 'Hello', createdAt: 1513337977055, createdBy: 'userId' }
child_added { message: 'Another message', createdAt: 1513337977066, createdBy: 'userId2' }
Note that I'm not adding new entries to Firebase here. Just querying.
EDIT: Here is a fiddle link demonstrating the issue: https://jsfiddle.net/dspLwvc3/2/
firebase here
You've uncovered a quite interesting edge case there. The behavior you're seeing is expected from how the system operates. But I think we can all agree that it's far from intuitive. :-/
It's essentially a race condition, combined with Firebase's guarantees about what it will and won't fire and when it fires events.
Essentially what happens is this:
Client Server
| |
(1)| --once('value'----> |
| |
(2)| -on('child_added'-> |
| |
| . |
| . |
| . |
| |
| value |
(3)| <------------------- |
| |
| child |
(4)| <------------------- |
| |
There are 4 key moments in there:
once('value')
listener for /messages/message1
. The client sends the request to the server and waits.You attach a on(-child_added
listener for the last-known key of /messages
. The client sends the request to the server and waits.
The response to the first request comes back from the server. At this stage there are two listeners. The once('value
listener is clear, so it fires and is removed. But at this point /messages/message1
is also the last-known key of /messages
, so the client fires that child_added
listener too.
/messages/message3
comes back from the server. There is only one listener left and it requested to hear about the last message, so it fires. Note that if you'd also have a listener for child_removed
, it would fore for /messages/message1
at this point.As I said, it's not very intuitive. But from the system's perspective it is the correct behavior. That means that you don't want this behavior, you will need to use the API in a different way. The simplest one with your current code would be to move attaching the child_added
listener into the once('value'
callback:
ref.child('messages/$groupId/$messageId')
.once('value')
.then(snap => {
console.log('value', snap.val()))
// Start new message listener
ref.child('messages/$groupId')
.orderByKey()
.limitToLast(1)
.on('child_added', snap => console.log('child_added', snap.val()))
})
This works because, by the time the child_added
listener is attached, the /messages/message1
snapshot has already been flushed from the client's cache.
Update (2018-01-07): another developer encountered this behavior and had a hard time maintaining the order of children. So I wrote up a bit more how this behavior (while unexpected) still maintains the correct order of the children. For more, see my answer here: Firebase caching ruins order of retrieved children
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With