5

So, I have an Angular 9 app hosted on Firebase and using Firestore for the data. I have an issue that seems very simple but I can't wrap my head around why it's happening. I've simplified the app a lot to find the root cause of this and will try to explain the issue as well as I can below.

The app: I have 2 pages, one Homepage, and one Transactions page. Both pages are reading from the same Firebase collection "transactions". However, on the Home page, I want to show the 4 most recent transactions (sorted by date, descending), whereas on the Transactions page I want to show the 10 most profitable transactions (sorted by amount, descending). For the time being, I'm just logging the data to the console for debugging. Before logging the data, I'm also manipulating it slightly (see code below).

The problem: when I start on the Homepage, I can see my 4 most recent transactions as expected in the console. However, when I go to the Transactions page, for a brief second it logs the 4 most recent transactions in the console again, which are supposed to be only shown on the Homepage. After a second or so, it shows the expected 10 most profitable transactions.

The code: Here is my code for the home.page.ts:

  txSubscription: Subscription;

  constructor(
    public afAuth: AngularFireAuth,
    private readonly firestore: AngularFirestore
  ) { }

  // Function to get the 4 most recent transactions
  async getRecentTransactions() {
    this.txSubscription = this.firestore
      .collection('transactions', ref => ref.orderBy('date', 'desc').limit(4))
      .valueChanges()
      .subscribe(rows => {
        this.recentTransactions = [];

        rows.forEach(row => {
          let jsonData = {};
          jsonData['ticker'] = (row['ticker'].length <= 10 ? row['ticker'] : row['ticker'].substring(0, 10) + '...');
          jsonData['date'] = formatDate(row['date'].toDate(), 'dd/MM/y', 'en');
    
          jsonData['amount'] = prefix + formatNumber(row['netAmount'], 'be', '1.2-2');
    
          this.recentTransactions.push(jsonData);
        })

        console.log("home page", this.recentTransactions);
      })
  }

  ngOnInit() {
    this.afAuth.onAuthStateChanged(() => {
      this.getRecentTransactions();
    })
  }

  ngOnDestroy() {
    this.txSubscription.unsubscribe();
  }

And the code for transaction.page.ts is very similar:

  txSubscription: Subscription;

  constructor(
    public afAuth: AngularFireAuth,
    private readonly firestore: AngularFirestore
  ) { }

  // Function to load the data for the home page
  loadHomeData() {
    this.txSubscription = this.firestore
      .collection('transactions', ref => ref.orderBy('profitEur', 'desc').limit(10))
      .valueChanges()
      .subscribe(rows => {
        this.resultRows = [];

        rows.forEach(row => {
          this.resultRows.push(row['ticker'].slice(0, 8));
        });

        console.log("transaction page", this.resultRows);
      })
  }

  ngOnInit() {
    this.afAuth.onAuthStateChanged(() => {
      this.loadHomeData();
    })
  }

  ngOnDestroy() {
    this.txSubscription.unsubscribe();
  }

The result: here's what is outputted to the console at every step

  1. I open the app on the Home page (4 rows as expected):
home page (4) [{…}, {…}, {…}, {…}]
  0: {ticker: "BAR", date: "21/07/2020", amount: "- € 1 086,10"}
  1: {ticker: "ASL C340.0...", date: "18/07/2020", amount: "€ 0,00"}
  2: {ticker: "ASL C340.0...", date: "14/07/2020", amount: "- € 750,85"}
  3: {ticker: "TUI C7.00 ...", date: "20/06/2020", amount: "€ 0,00"}
  length: 4
  __proto__: Array(0)
  1. I navigate to the Transactions page:
transaction page (4) ["TUI C7.0", "ASL C340", "BAR", "ASL C340"]

transaction page (10) ["ASL C240", "ASL C232", "REC", "ASL C270", "ASL C310", "ASML", "ASL P220", "BAR", "CFEB", "MELE"]

Why, when navigating to the Homepage, is it showing again the same 4 rows from the Homepage in the console?

Mihir Ajmera
  • 127
  • 1
  • 9
vdvaxel
  • 667
  • 1
  • 14
  • 41
  • 1
    Did you enable persistence with the Firestore SDK? – Doug Stevenson Aug 11 '20 at 17:14
  • Hi @DougStevenson, could you elaborate? What does that mean exactly? – vdvaxel Aug 11 '20 at 17:16
  • https://firebase.google.com/docs/firestore/manage-data/enable-offline – Doug Stevenson Aug 11 '20 at 17:21
  • No, I don’t have it enabled. – vdvaxel Aug 11 '20 at 18:28
  • It should happen automatically, but maybe clear the data arrays in the OnDestroy(); I assume the data coming from Firestore is via a service, and they are likely singletons in the main module, so they stay alive through the whole application. – Steven Scott Aug 13 '20 at 14:36
  • Hi @StevenScott, in the subscribe part of my code I always initialize an empty array. Shouldn’t that be enough? – vdvaxel Aug 14 '20 at 13:51
  • I would think so, I am just trying to suggest things to help, as your code looks OK to me (again, I do not use Firestore and work with Observables directly from Angularfire) as I use the tools differently, but am also using more basic interactions. Trying to help point you to something to try, in case it shows you the issue or gives you a though on next steps. – Steven Scott Aug 14 '20 at 14:57

3 Answers3

0

I think to problem lays here:

loadHomeData() {
  this.txSubscription = this.firestore
  ...
  .subscribe(rows => {
    ...
  })
}

ngOnInit() {
  this.afAuth.onAuthStateChanged(() => {
    this.loadHomeData();
  })
}

ngOnDestroy() {
  this.txSubscription.unsubscribe();
}

You are correctly unsubscribing. However, look at what happens when onAuthStateChanged fires again. Your first subscription is lost and there is no way to unsubscribe. I think using the switchmap operator on onAuthStateChanged will fix your problem. Something like this:

this.subscription = this.af.authState.pipe(
  switchMap(auth => {
    if(auth !== null && auth !== undefined){
      return   this.firestore.collection('transactions', ref => ref.orderBy('profitEur', 'desc').limit(10)).valueChanges())
    } else{
      throw "Not loggedin";
    }
).subscribe(...)
Robin Dijkhof
  • 18,665
  • 11
  • 65
  • 116
  • Hi Robin, many thanks for your suggestion. It seems to work with your solution, at least on the transactions page I'm not seeing the 4 rows from the home page anymore. Would you recommend not using onAuthStateChanged at all? The reason I used is was because there were permissions errors when I switched pages. I'm not sure if this is the best solution though... – vdvaxel Aug 19 '20 at 17:00
  • Hi Robin, I've followed this tutorial (https://dev.to/dobis32/user-authentication-with-angular-angularfire-3eja) w.r.t. authentication and loading data when authenticated. However, I'm still getting the same issue. Do you know what the issue would be with this tutorial? – vdvaxel Aug 19 '20 at 20:40
  • I think you can still use the onAuthStateChange like you did in your example, but you should check if you have an active subscription and unsubscribe before loading the home data. Personally I favor the switchmap solution because I am a fan of rxjs. I think you get the permission error because the switchmap does not check whether the user is logged in and neither do you in you auth state changed. I though logging out and refreshing the token does also trigger the state. – Robin Dijkhof Aug 19 '20 at 21:07
  • @vdvaxel I updated my example. Not sure what wrong with the tutorial. – Robin Dijkhof Aug 19 '20 at 21:18
  • Hi Robin, thanks for the reply, but does this mean I would have to use the onAuthStateChanged and switchMap for EVERY read operation in my app? It just doesn’t feel right and I have the feeling the core problem lies somewhere else. – vdvaxel Aug 19 '20 at 22:07
  • You could create a router guard. – Robin Dijkhof Aug 20 '20 at 06:20
  • In the tutorial they're doing this. Isn't that the same as what you're doing in your 2nd code block? ``` this.userAuth = this.fs.signedIn.subscribe((user) => { if (user) { this.getTaskData(); } else { this.router.navigate([ 'signin' ]); } }); ``` – vdvaxel Aug 20 '20 at 07:30
0

You get results 2 times on transactions page. It is very likely valueChanges returns data from firestore in-memory cache as a quick response and then it reads from real data. In your case 4 rows are cached when they are returned on home page and they are processed from cache on transactions page.

Kudratillo
  • 190
  • 11
  • And do you know how to tell Firebase to NOT read from the cache? – vdvaxel Aug 17 '20 at 17:34
  • @vdvaxel I suggest you to check this answer https://stackoverflow.com/questions/52700648/how-to-avoid-unnecessary-firestore-reads-with-cache – Kudratillo Aug 18 '20 at 13:23
  • From what I can see in that question, Firestore will always read from the server first before going to the cache? – vdvaxel Aug 19 '20 at 16:49
  • yeah, meaning try to use `get` instead of `valueChanges` – Kudratillo Aug 20 '20 at 09:44
  • 1
    Seems to be a known issue: https://github.com/angular/angularfire/issues/2012 . And agree, the recommended workaround seems to be "use `get` instead of `valueChanges`" – amakhrov Aug 21 '20 at 00:36
  • @vdvaxel Have you tried to change `valueChanges` to `get`? – Kudratillo Aug 23 '20 at 03:57
  • Hey, I haven’t tried it yet. From what I see, get is used if you just want to get the data once, valueChanges will automatically update when new data arrives? So get might not be the best solution for me I think, if I want the data to update automatically. – vdvaxel Aug 23 '20 at 10:39
  • @vdvaxel If your code can handle multiple updates then no need to worry about this "cache" issue. User isnʼt supposed to notice it. Otherwise you may try `throttle`+`distinctUntilChanged` operators combination to get the latest value in very short period of time (e.g. 200ms). – Kudratillo Aug 24 '20 at 15:02
0

This issue is happening maybe because of

this.afAuth.onAuthStateChanged()

getting triggered twice.

Instead of checking for the auth state on every component. you can simply subscribe to the auth state at app.component.ts.if a user is unauthenticated or auth state changes it will redirect to login page otherwise it will redirect to home.page.ts

export class AppComponent {
  constructor(private readonly auth: AngularFireAuth, private router: Router) {
    this.auth.authState.subscribe(response => {
      console.log(response);
      if (response && response.uid) {
        this.router.navigate(['dashboard', 'home']); //Home page route
      } else {
        this.router.navigate(['auth', 'login']); //Login page route
      }
    }, error => {
      this.auth.signOut();
      this.router.navigate(['auth', 'login']); //Login Page route
    });
  }
}

In your home.component.ts & transaction.page.ts no need to checking the auth state.

  • home.component.ts
 txSubscription: Subscription;

  constructor(
    public afAuth: AngularFireAuth,
    private readonly firestore: AngularFirestore
  ) { }

  // Function to get the 4 most recent transactions
  async getRecentTransactions() {
    this.txSubscription = this.firestore
      .collection('transactions', ref => ref.orderBy('date', 'desc').limit(4))
      .valueChanges()
      .subscribe(rows => {
        this.recentTransactions = [];

        rows.forEach(row => {
          let jsonData = {};
          jsonData['ticker'] = (row['ticker'].length <= 10 ? row['ticker'] : row['ticker'].substring(0, 10) + '...');
          jsonData['date'] = formatDate(row['date'].toDate(), 'dd/MM/y', 'en');
    
          jsonData['amount'] = prefix + formatNumber(row['netAmount'], 'be', '1.2-2');
    
          this.recentTransactions.push(jsonData);
        })

        console.log("home page", this.recentTransactions);
      })
  }

  ngOnInit() {
      this.getRecentTransactions();
  }

  ngOnDestroy() {
    this.txSubscription.unsubscribe();
  }
  • transaction.page.ts
 txSubscription: Subscription;

  constructor(
    public afAuth: AngularFireAuth,
    private readonly firestore: AngularFirestore
  ) { }

  // Function to load the data for the home page
  loadHomeData() {
    this.txSubscription = this.firestore
      .collection('transactions', ref => ref.orderBy('profitEur', 'desc').limit(10))
      .valueChanges()
      .subscribe(rows => {
        this.resultRows = [];

        rows.forEach(row => {
          this.resultRows.push(row['ticker'].slice(0, 8));
        });

        console.log("transaction page", this.resultRows);
      })
  }

  ngOnInit() {
      this.loadHomeData();
  }

  ngOnDestroy() {
    this.txSubscription.unsubscribe();
  }
Mihir Ajmera
  • 127
  • 1
  • 9
  • Hello, I'm currently trying your suggest solution, thanks for your detailed response. I do have one question though: when I start the app and I arrive on the login screen, but I change the URL manually from /login to /home, I can still see the home page. Isn't it supposed to redirect me to the /login page instead, based on the code you suggested for app.component.ts? – vdvaxel Aug 21 '20 at 13:21
  • Hello again, I just tried your entire solution but I'm still getting the same issue as explained in my initial post (i.e. seeing the data from the home page in the console when navigating to the transactions page). – vdvaxel Aug 21 '20 at 13:30
  • Any idea on this @Saurabh47g? – vdvaxel Aug 26 '20 at 19:36
  • For checking users authentication in case of manually entering url.you can use canActivate route guard. I will update my answer with example. I'm not sure about why data getting during navigation. – Saurabh Gangamwar Aug 26 '20 at 21:14