2

I have an Angular app with a Home page where I show the 4 latest rows in the "transactions" Firebase collection (ordered by date, descending). Then there's a separate Transactions page where I show the top 10 rows in this collection (ordered by amount, descending). However, when I start on the Home page and then go to the Transactions page, in my bar chart which should show the top 10 transactions by amount, I still see the 4 most recent transactions from the Home page.

Demo link: https://tickrs-app.web.app/

Steps to reproduce:

  1. open the demo app
  2. on the home page at the very bottom, you will see "Recent transactions"
  3. open the menu and navigate to the "Transactions" page
  4. the bar chart will look a bit strange, it seems the data still contains the 4 recent transactions from the home page
  5. navigate to a different page (not the Home page) and then back to the "Transactions" page, the bar chart should look normal now

Here's my code for the home.page.ts:

  // Function to load the 4 most recent transactions to show on the home page
  async loadData() {
    // Order by date, descending
    const orderParamsDateDesc = {
      field: 'date',
      order: 'desc'
    }
    
    // Call our service to load the data, given the ordering details, and limit the number of rows to 4
    await this._FirebaseService.readSortLimit('transactions', orderParamsDateDesc, 4).then(result => this.transactionRows = result);
  }

  async ngOnInit() {
    // Only try to load the data if the user is authenticated again
    this.afAuth.onAuthStateChanged(async () => {
      await this.loadData();
    })
  }

Here's the same code for the transaction.page.ts:

  // Function to load the top 10 transactions, ordered by amount (descending)
  async getRows() {
    // Initialize the arrays
    this.barChartDataEur = [];
    this.barChartLabelsEur = [];
    let rows: any = [];

    // Order by amount, descending
    let orderParams = {
      field: 'amount',
      order: 'desc'
    }

    // Call our service to load the data given the ordering details, and limit the number of rows to 10
    await this._FirebaseService.readSortLimit("transactions", orderParams, 10).then(result => rows = result);

    // Loop over the resulting rows and load the stock tickers and amount separately in the arrays which will be used for the bar chart
    await rows.forEach(row => {
      this.barChartLabelsEur.push(row.ticker.slice(0, 8));
      this.barChartDataEur.push(row.amount);
    });

    // Set the loaded flag to true
    this.loaded = true;
  }

  ngOnInit() {
    // Only execute this part if user is authenticated
    this.afAuth.onAuthStateChanged(async () => {
      this.getRows();
    })
  }

Here's part of the transaction.page.html to render the bar chart:

  <div class="chart-canvas">
    <canvas baseChart *ngIf="loaded"  // Only if data is loaded
            [data]="barChartDataEur"
            [labels]="barChartLabelsEur"
            [chartType]="barChartType"
            [options]="barChartOptions"
            [colors]="barChartColors"
            [legend]="barChartLegend"
            [plugins]="barChartPlugins">
    </canvas>
  </div>

Here is my firebase.service.ts with the readSortLimit function that is used on both pages:

  // Input: name of the Firebase collection, the ordering details and the number of rows to return
  readSortLimit(collection, orderDetails, limitNumber) {
    return new Promise((resolve, reject) => {
      let result = [];
      this.firestore
        .collection(collection, ref => ref
          .orderBy(orderDetails.field, orderDetails.order)
          .limit(limitNumber)
        )
        .snapshotChanges()
        .subscribe(item => {
          Array.from(item).forEach(row => {
            result.push(row.payload.doc.data());
          });
          resolve(result);
        });
    });
  }
vdvaxel
  • 667
  • 1
  • 14
  • 41
  • I use Angularfire for getting data to the pages, so I am not that familiar with working with Firebase native functions. However, it would appear that you are likely not seeing the new records in Firebase when they change. Is the `resolve(result);` not finalizing the promise, so your data is loaded, but you are not still listening for changes on the observable? – Steven Scott Aug 07 '20 at 15:07
  • Hey, I'm actually using @angular/fire as well. I'm not sure what's happening - on the home page I read the top 4 rows from my collection, which is working fine. Then, however, on the next page instead of the top 4 I want the top 10. Instead of getting the top 10, I still see the same 4 from the home page PLUS the top 10 that were read from Firestore... – vdvaxel Aug 07 '20 at 16:15
  • It appears that you are putting the data into an array, so you are adding the 10 on the second page to the original 4 from the first page. Why not simply keep returning the collection and using the async pipe? – Steven Scott Aug 10 '20 at 00:52
  • Hi @StevenScott, I've edited my post with some more details and also added a demo link of my app to give a better idea of the issue. Basically, on both pages I need to show a separate dataset. Both are coming from the "transactions" collection in Firebase, but the one for the Home page is ordered by date and the one on the Transactions page is ordered by amount. – vdvaxel Aug 10 '20 at 08:51
  • Interesting. I see what you mean. When I switch initially, the values only have the 4 records, not the 14. I switched to table view to better understand the data. When I move in the pages, then it does refresh properly. The reload on the menu, also converts this to 10 entries. The only difference I have used fro you (albeit I use the Firebase Real time DB) is that I use ValueChanges() not SnapshotChanges() when working with a list of items. – Steven Scott Aug 10 '20 at 17:00
  • I still think it might be in the promise. Your readSortLimit() returns a new promise, from an array built internally. Angularfire and the real time database, Observables are used. They do not actual fire against the engine until subscribed to. I use them on the page with the async() pipe which does the subscribe/unsubscribe automatically. As you are just rendering data (from what I can quickly see) the docs do recommend ValueChanges() `https://github.com/angular/angularfire/blob/master/docs/firestore/collections.md` but this should not be the source of your problem. – Steven Scott Aug 10 '20 at 17:10
  • This Stack Overflow post `https://stackoverflow.com/questions/48608769/what-is-snapshotchanges-in-firestore` indicates that the snapshot is an observable that is returned. I have had issues in my code (likely my fault, not Angularfire) but I normally point a reference to the resource `const Ref = this.firestore.collection(collection, ref => ref.orderBy(orderDetails.field, orderDetails.order).limit(limitNumber)).snapshotChanges();` and then `const Data = Ref..subscribe(item => {Array.from(item).forEach(row => {result.push(row.payload.doc.data());});resolve(result);});` – Steven Scott Aug 10 '20 at 17:14
  • Hi @StevenScott, many thanks for your answers. I’ll take a look at the link in the morning. Just a question: what difference does it make if I point a reference to the resource first and then load the data from it? – vdvaxel Aug 10 '20 at 21:44
  • @StevenScott I've tried to follow the same approach in the GitHub link you sent, i.e. using valueChanges() instead of snapshotChanges(), defining the observable in the constructor and then subscribing to this observable to load the data. However, instead of seeing the 4 rows from the Home page on the Transactions page, I now see 14 rows on the Transactions page (the 4 from the Home page + the 10 that are supposed to show on the page). Any ideas? – vdvaxel Aug 11 '20 at 14:03
  • As I said, I have not use Firestore. I have seen the method used where the reference is created, then the subscription is used to monitor the data, since it is the action of the subscription that causes the data fetch to happen. Nothing is retrieved in an Observable (rxjs) until it is subscribed to. Out of curiosity, on a small sample, could you simply try a simple test with your service returning the observable, and then a basic listing on the page using the async pipe? The async pipe handles the subscription, destruction when done, etc. This might help show how the data is coming back. – Steven Scott Aug 12 '20 at 20:25
  • @StevenScott I’ve simplified my app significantly to find the root cause of the issue. Check this post for an update: https://stackoverflow.com/questions/63363151/angular-app-with-firebase-data-why-am-i-seeing-data-from-the-previous-page?noredirect=1#comment112043323_63363151 – vdvaxel Aug 13 '20 at 09:18

1 Answers1

1

snapshotChanges probably processes and returns data from cache firstly as a quick result. Your function readSortLimit returns a promise and the promise is resolved with data from cache. Next resolves are just ignored.

You need to modify function readSortLimit to return Observable instead.

  readSortLimit(collection, orderDetails, limitNumber) {
    return this.firestore
        .collection(collection, ref => ref
          .orderBy(orderDetails.field, orderDetails.order)
          .limit(limitNumber)
        )
        .snapshotChanges()
        .subscribe(items => {
          Array.from(items).map(row => row.payload.doc.data()));
        });
  }

And then modify getRows

  async getRows() {
    // Order by amount, descending
    let orderParams = {
      field: 'amount',
      order: 'desc'
    }

    // Call our service to load the data given the ordering details, and limit the number of rows to 10
    this._FirebaseService.readSortLimit("transactions", orderParams, 10)
           .subscribe(rows => {
             // Initialize the arrays
             this.barChartDataEur = [];
             this.barChartLabelsEur = [];

             // Loop over the resulting rows and load the stock tickers and amount separately in the arrays which will be used for the bar chart
             rows.forEach(row => {
               this.barChartLabelsEur.push(row.ticker.slice(0, 8));
               this.barChartDataEur.push(row.amount);
             }); 
             
             // Set the loaded flag to true
             this.loaded = true;            
           });
  }

**Make sure getRows called once at most. Otherwise you get subscribed to same event multiple times.

Kudratillo
  • 190
  • 11