angularfire: unexpected: AngularFirestore#collection() returns cached value on initial subscription

Version info

Angular: 7.2.5 Firebase: 5.8.3 AngularFire: 5.1.1 Other (e.g. Ionic/Cordova, Node, browser, operating system): node 10.14.2, macOs X

How to reproduce these conditions

I’ve had some trouble isolating the exact problem. Basically, I have a people collection. If I fetch an individual document from the people collection (lets call it the person A doc), and then subscribe to the entire people collection, I’m seeing that the collection('people').valueChanges() subscription immediately returns an array that just contains person A. The subscription then fires again shortly later with the full results of the person collection.

If I invoke collection('people').valueChanges() before fetching an individual document from the people collection, then collection('people').valueChanges() returns the correct results on the first try.

What I think is happening, is that collection('people') is immediately returning results from an in-memory cache and then updating those results when it hears back from the server. I could see scenarios where this behavior would be desirable, but it is not desirable in my case, was unexpected and hard to diagnose, and there doesn’t seem to be an option to disable it. I’m not sure if this is intended behavior or a bug.

I think it’s possible that issue #1975 is being caused by the same problem.

In my case, the problem with this behavior (other than the fact that it is unexpected) is that I want to take(1) from the collection('people').valueChanges() query, except now the return value of that action is indefinite.

Debug output

I have none. I did try to set firebase.database().enableLogging(true); as suggested, but this.angularFirestore.app.database() does not have an enableLogging() function.

Expected behavior

By default, I expected query results to not be returned unless they accurately reflect the query.

I could see value in returning results immediately from a cache, but I don’t think this behavior should be the default as it’s unexpected, hard to diagnose, and can lead to unwanted behavior.

Actual behavior

AngularFirestore#collection() seems to return results immediately from the cache and then update the results when it hears back from the server.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 20
  • Comments: 43 (12 by maintainers)

Commits related to this issue

Most upvoted comments

I have been hit by this very badly. In my opinion it is a bug.

example:

you have collection where you have following docs: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

If you have open subscription to item no 9 and you make query where you limit to 5 first items with take(1) you will receive item [9] instead of [0, 1, 2, 3, 4].

Sample project: https://angular-f6nfor.stackblitz.io

This makes e.g. pagination queries unreliable. When you relay that last item received will be last item of your previous pagination query and you use this item as startAfter for next query.

I’m running into this as well.

The valueChanges documentation currently states that this method returns “the current state of your collection.” The documentation does not say anything about the potential for partial or incomplete state from local cache being emitted first. I agree with what others have said about the current behavior being counterintuitive and unexpected.

I view the current behavior as a bug, and my strongest preference would be for the current behavior to be fixed. If the Firebase team does not consider the current behavior to be a bug, at the very least, can the valueChanges documentation be updated to describe the current behavior (how the first valueChanges emission might be incomplete and from local cache), as well as how it can be worked around? Otherwise, I think this behavior will continue to surprise people.

This is how the Firestore JS SDK works. I’m considering an API change that would allow you to inspect the metadata and say filter { snap -> !snap.metadata.fromCache } on snapshotChanges()

@choopage I don’t think that provided solution completely solves the problem that was described originally, but I so far came up with the following solution to skip the first cached push:

 public collectionSnapshotChanges(query?: QueryFn): Observable<Array<T>> {
        return this.getPrivateCollectionRef(query).pipe(
            mergeMap(ref => ref.snapshotChanges()),
            filter((actions, index) => index > 0 || !actions.every(({payload: {doc: {metadata}}}) => metadata.fromCache)),
            map(actions => actions.map(({payload: {doc}}) => ({...doc.data(), id: doc.id}))),
        );
    }

Metadata is now included in snapshots in 6.1, so you can filter out the cache if you want.

Anything would help! Glad someone brought up my old issue from a while ago.

@DogAndDoll Correct this doesn’t solve the “problem” as this is Firestore behaving as designed, this is however a much better work around. Thanks for typing up the example.

@hiepxanh in my case for example, I have a list of 30 tickets, when you click one, you open the details of that ticket. Then we you go back to the list of 30, first 1 appears, and then the 30. That is not the expected behavior.

I am not sure about the best solution so I think the easiest way is to expose information about if the record is served from the cache or not and then let the developer take control.

@hiepxanh not sure why you feel an example repository is necessary. The behavior @leandroz describes (which this issue is specifically addressing) is not in question. It was confirmed as “working as intended” by @jamesdaniels in a comment above. James also proposed a solution to address this issue in that comment, which (I think) would close this issue from a technical perspective (separately, these is also the issue of “surprise,” which could be addressed by some documentation improvements).

my workaround: Remember this only use for get data one time, cannot listen realtime

.ts
// for note purpose:
this.dbft.collection('users')
  // .valueChanges() // NOTE: remember, we not using this anymore, change to get from server and pipe snapshot => snapshotData
  .get({ source: 'server' })
  .pipe(
    map(actions => {
      const data = [];
      actions.forEach(a => {
        const item = a.data() as Appointment;
        data.push(item);
      });
      return data;
    })
  )
  .subscribe(value => console.log('fresh value', value));

Yeah I know but get() doesn’t provide live reloading, unless I did something wrong. If static content is what you need, then get() by itself is fine. The duplicate read cost is what I found to work with live reloading. If you can do that with get() though, please correct me.

I’m having this same issue but with the first valueChanges() being for an auth user, and the second valueChanges() to display all users. It displays the logged in user first, and then after a second or two displays all of them. I’m using ionic 4 and angular 6, but I can’t figure out how to use the get() method as suggested. It seems to return metadata. Does anyone have another solution? I need this asap for work haha.

I use the get() call for one of the queries and snapshotChanges() for the other.

Sounds like another good workaround without adding additional index 😃

@jamesdaniels I can’t speak for everyone, but your proposed update would address the problems I am experiencing.

Experiencing this bug too!