Skip to content

snapshotChanges trigger twice #2336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
eliavmaman opened this issue Feb 25, 2020 · 19 comments
Closed

snapshotChanges trigger twice #2336

eliavmaman opened this issue Feb 25, 2020 · 19 comments

Comments

@eliavmaman
Copy link

eliavmaman commented Feb 25, 2020

Version info

Angular:8

AngularFire:"5.2.1

I have main component that retrieve data from collection and i have sub component that retrieve different data from the same collection.

whats happened is that the sub-component snapshotChanges() trigger twice with the data it should retrieve and the data that retrieved by the parent component.

Parent component:

this.firestore.collection('galleries', ref => ref.where('user', '==', this.user.id)
      .orderBy('date', 'desc'))
      .snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as any;
        data.id = a.payload.doc.id;
        return data;
      }))).subscribe((sessions) => {
         console.log(sessions);
      });

Child component:

this.firestore.collection('galleries', ref => ref.limit(6).orderBy('date', 'desc'))
      .snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as any;
        data.id = a.payload.doc.id;
        return data;
      }))).subscribe((sessions) => {console.log(sessions)})

in the child component i've limit the query to 6 document ' but it's subscription called twice, and displays both parent data child documents.
Any idea?

@eliavmaman
Copy link
Author

Any one?

@jimmykane
Copy link

I think I am facing the same

@eliavmaman
Copy link
Author

Yes it's like super basic and no one complains about it

@jimmykane
Copy link

#1552

So you might be getting first the cache and then the server results

@eliavmaman
Copy link
Author

It's not the same

@mosesweb
Copy link

mosesweb commented Mar 9, 2020

sure you are not subscring through an asyncpipe in the component template?

@jimmykane
Copy link

I am now also facing this issue @jamesdaniels and others

@sumpton
Copy link

sumpton commented Apr 18, 2020

I believe I have the same problem. I have a component (The Problem) that returns the correct results consistently when run but itself, or after any other component except one (The Cause). When it runs after this component, the query will first return the records from The Cause, and then return the correct records.

A pipe/tap to console shows the double batches of records returning on the single query. The results are identical with valueChanges or snapshotChanges. Both components are using the same service, but different queries. The Cause is looping through doc ids and "getting" each to hydrate the entity, where The Problem is a query with orderBy, startAfter, and limit.

I find it interesting that the problem respects "limit". If The Cause has a limit larger than The Problem, the smaller limit will be returned followed briefly by the correct records and limit.

I tried to setup a simple demo, with three components, but it works as it should. I don't see what is different with The Problem and The Cause.

@jimmykane
Copy link

jimmykane commented Apr 18, 2020 via email

@kyleabens
Copy link

My solution is kind of hacky but I include auditTime(2000) in the pipe to provide enough time for that second query to come through.

this.collection$(path, query).pipe(auditTime(2000),take(1)).toPromise();

@sumpton
Copy link

sumpton commented Apr 26, 2020

@kyleabens I appreciate you sharing your hack. It works for me. 500ms works in my instance currently.

@jimmykane I don't understand using "get".

@ondokuzz
Copy link

ondokuzz commented May 2, 2020

i have the same problem.

this occurs when i do a snapshotChanges() on a collection, while there is already another active snapshotChanges() on the same collection. the late coming subscription first receives the results cached from the former subscription. then after a while it receives the actual results.

i think this is a very problematic design. when i execute a query, it should return me only once, not twice. i shouldn't be trying to handle two different results from one query. this either complicates my code, or makes me disable some features i would like to present to my users.

the cached results should be returned only when firestore servers can not be contacted, that is, the query should return either the correct result or cached result. not both.

@EdRW
Copy link

EdRW commented May 16, 2020

I think it would be fine that it returns twice, if there were also a way for me to differentiate where the data came from. Then I could decide for myself when to display cached results.

@KingDarBoja
Copy link
Contributor

KingDarBoja commented Aug 25, 2020

This is answered at firebase/firebase-js-sdk#2976, which is a desired behaviour.

You can filter it by using the snapshot.payload.doc.metadata.fromCache property on the snapshots from snapshotChanges() or any other method call that uses DocumentChangeAction.

@TomasMasiulis
Copy link

This is my solution using RxJS operator distinctUntilChanged and lodash isEqual function.
With it you can avoid twice triggering and get benefits from the cache usage

this.afs.collection(path).doc<any>(id).snapshotChanges()
  .pipe(distinctUntilChanged((prev, curr) => _.isEqual(prev?.payload.data(), curr?.payload.data())))

similar for valueChanges

this.afs.collection(path).doc<any>(id).valueChanges()
  .pipe(distinctUntilChanged((prev, curr) => _.isEqual(prev, curr)))

@mauriciogracia
Copy link

This might help - https://stackoverflow.com/questions/67514628/angular-firestore-subscribe-fires-twice/67514861#67514861

@EmreAkkoc
Copy link

This is my solution using RxJS operator distinctUntilChanged and lodash isEqual function. With it you can avoid twice triggering and get benefits from the cache usage

this.afs.collection(path).doc<any>(id).snapshotChanges()
  .pipe(distinctUntilChanged((prev, curr) => _.isEqual(prev?.payload.data(), curr?.payload.data())))

similar for valueChanges

this.afs.collection(path).doc<any>(id).valueChanges()
  .pipe(distinctUntilChanged((prev, curr) => _.isEqual(prev, curr)))

@TomasMasiulis
what would be the equivalent while doing snaphotChanges() or valueChanges() on Collection rather than Document?
or is it not possible?

@madmacc
Copy link

madmacc commented Apr 26, 2023

@fouchekeagan
Copy link

TomasMasiulis what would be the equivalent while doing snaphotChanges() or valueChanges() on Collection rather than Document? or is it not possible?

I did this by setting a "lastUpdated" timestamp on the documents I was querying in my collection and extended the distinctUntilChanged(...) operator, like this:

ValueChanges:

this.afs.collection('yourCollectionPath', ref => ref.where(...)).valueChanges().pipe(
    distinctUntilChanged((a, b) => {
        // If the size of the query result has changed, there are changes.
        if (a.length !== b.length) return false;

        // Do your custom compare here.
        // I used the timestamp since my documents often updated only once every 12 hours.
        // Returning `false` will continue the changes to your subscription.
        for (let i = 0; i < a.length; i++) {
            const previous = a[i], current = b[i];
            if (previous.lastUpdated.toDate().getTime() !== current.lastUpdated.toDate().getTime()) { 
                return false;
            }
        }

        // If no changes are detected, return `true` to ignore this emission.
        return true;
    })
)

For SnapshotChanges, this will be very similar, the only difference would be how you access the data - the only adjustment required is how you set the previous and current variables.
The same code as above, with only the forloop adjusted for SnapshotChanges:

...
for (let i = 0; i < a.length; i++) {
    const previous = a[i].payload.doc.data(), current = b[i].payload.doc.data();
    if (previous.lastUpdated.toDate().getTime() !== current.lastUpdated.toDate().getTime()) return false;
}
...

I realize that not all scenarios you will be able to set a lastUpdated field and handle it the way I have. However, you are able to implement the same pattern and change how you determine the distinctUntilChanged to evaluate to false to get the same effect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests