1

The following component uses a service to fetch data from a server with the HttpClient module. The service basically works. When an individual bankaccount is selected the BankaccountListComponent is destroyed and the BankaccountEditComponent displayed. When clicking on save the method saveBankaccount in the BankaccountEditComponent is executed (see below). The data is sent to the server and stored (always). After saving the data the BankaccountListComponent is displayed. In ngOnInit it should fetch again the data.

The problem is the ngOnInit of the BankaccountListComponent is executed (always) but the data is not fetched always. It does not work all the time and I cannot figure out why.

From BankaccountEditComponent:

  saveBankaccount() {
    this.subscription = this.route.params
      .subscribe(params => {
        const id = (params['id'] || '');
        if (id) {
          this.bankaccountService.update(id, this.bankaccount).subscribe(bankaccount => {
            this.bankaccount = bankaccount;
          });
        } else {
          this.bankaccountService.add(this.bankaccount).subscribe(bankaccount => {
            this.bankaccount = bankaccount;
          });
        }
        const relUrl = this.router.url.includes('edit') ? '../..' : '..';
        this.router.navigate([relUrl], { relativeTo: this.route });
      });
  }

All of BankaccountListComponent:

import { Component, OnInit, OnDestroy } from '@angular/core';

import { BankaccountService } from '../../services/bankaccount.service';
import { Bankaccount } from '../../domain/bankaccount';

@Component({
  selector: 'ac-bankaccount-list',
  templateUrl: './bankaccount-list.component.html',
  styleUrls: ['./bankaccount-list.component.css']
})
export class BankaccountListComponent implements OnInit, OnDestroy {

  bankaccounts: Bankaccount[];
  bankaccountSelectedId: number;

  constructor(private bankaccountService: BankaccountService) { }

  ngOnInit() {
    console.log('init BankaccountListComponent');
    this.getBankaccounts();
  }

  ngOnDestroy() {
    console.log('destroying BankaccountListComponent');
  }

  getBankaccounts() {
    this.bankaccountService.getBankaccounts().subscribe(bankaccounts => {
      this.bankaccounts = bankaccounts;
      console.log('this.bankaccount: ' + Array.prototype.map.call(this.bankaccounts, function(bankaccount) { return bankaccount.name; }).join(", "));
    });
  }

  selectBankaccount(bankaccountId: number) {
    this.bankaccountSelectedId = bankaccountId;
    console.log('id of bankaccount selected: ' + this.bankaccountSelectedId);
  }

  deleteBankaccount(bankaccountId: number) {
    console.log('id of bankaccount to delete: ' + bankaccountId);
    this.bankaccountService.delete(bankaccountId).subscribe(_ => {
      this.getBankaccounts();
    });
  }

}

All of BankaccountService:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import 'rxjs/add/operator/map';

import { Bankaccount } from '../domain/bankaccount';

@Injectable()
export class BankaccountService {

  private headers = new HttpHeaders();
  private bankaccountsUrl = 'http://localhost:8090/account/accounts/';

  constructor(private httpClient: HttpClient) {
    this.headers = this.headers.set('Content-Type', 'application/json');
    this.headers = this.headers.set('Accept', 'application/json');
  }

  getBankaccounts(): Observable<Bankaccount[]> {
    return this.httpClient.get<Bankaccount[]>(this.bankaccountsUrl).map((result: any) => {
      console.log('fetched ' + result._embedded.accounts.length + ' bankaccounts from server');
      return result._embedded.accounts;
    });
  }

  getBankaccount(id: number): Observable<Bankaccount> {
    return this.httpClient.get<Bankaccount>(this.bankaccountsUrl + id).map((result: any) => {
      console.log('fetched bankaccount with id ' + result.id + ' from server');
      return result;
    });
  }

  update(id: number, bankaccount: any): Observable<Bankaccount> {
    return this.httpClient.put<Bankaccount>(this.bankaccountsUrl + id, bankaccount);
  }

  add(bankaccount: Bankaccount): Observable<Bankaccount> {
    return this.httpClient.post<Bankaccount>(this.bankaccountsUrl, bankaccount);
  }

  delete(id: number): Observable<Bankaccount> {
    console.log('will delete bankaccount ' + this.bankaccountsUrl + id);
    return this.httpClient.delete<Bankaccount>(this.bankaccountsUrl + id);
  }

}
Pac0
  • 21,465
  • 8
  • 65
  • 74
Mfried
  • 57
  • 2
  • 9
  • I can see that you are nesting observable subscriptions, which is not a good practice. Not sure if it's related or not to your issue, though – Pac0 Mar 14 '18 at 13:44
  • Are you using *ngFor for displaying data? Did you already try to put a `console.log()` under `this.bankaccounts = bankaccounts;` for visualize the data inside `this.bankaccounts` ? – Lorenzo Imperatrice Mar 14 '18 at 13:44
  • @Lorenzo Yes, I am using *ngFor. There is already a console.log statement in method getBankaccounts()!? – Mfried Mar 14 '18 at 14:11
  • @Pac0 Don't know how I would change that to still work!? – Mfried Mar 14 '18 at 14:14
  • @Mfried And sometimes log something and sometimes not right? – Lorenzo Imperatrice Mar 14 '18 at 14:18
  • @LorenzoImperatrice The log is always executed. And I can see in the log of the server that no select is being made. As though as this.getBankaccounts() would not be executed. – Mfried Mar 14 '18 at 14:23
  • @Mfried really a weird situation that you get the log (so the method is executed) but doesn't always fetch data. My best guess at this point is that, as Pac0 already said, 2 subscription creates a weird interaction. – Lorenzo Imperatrice Mar 14 '18 at 14:52
  • Yes, it reminds me of a bug I had exactly because of that, with nearly the same symptoms (fetch sometimes all right, sometimes fail). I will write a potential answer now. – Pac0 Mar 14 '18 at 14:53
  • It may be that you are not cleaning up properly. See if you have a similar issue to this: https://stackoverflow.com/questions/49223594/how-to-call-a-function-eveytime-a-component-is-opened/49242099#49242099 – jornare Mar 14 '18 at 15:04
  • @jornare In the BankaccountEditComponent I have an OnDestroy method which is executed and which calls `this.subscription.unsubscribe();`. Correct me if I am wrong but "not forever" subscriptions do not need to be unsubscribed manually (as e.g. in BankaccountListComponent#getBankaccounts)!? – Mfried Mar 14 '18 at 17:30
  • @Mfried What if you call saveBankaccount() twice? What happens to the first subscription? – jornare Mar 14 '18 at 20:31
  • @jornare See below, solution found. Thanks for your help, too! – Mfried Mar 17 '18 at 08:21

1 Answers1

1

(Rewritten after comments)

Chain Observables instead of nesting subscriptions

First of all, it's bad practice to nest subscriptions.

Among other reasons, because of readability. It becomes hard to tell which actions will be executed in which order.

First fix : use flatMap operator to chain Observables.

this.subscription = this.route.params
  .flatMap(params => {  // use 'flatMap' instead of 'subscribe' here
    const id = (params['id'] || '');
    if (id) {
      // here return the Observable that goes next instead of subscribing
      return this.bankaccountService.update(id, this.bankaccount);
    } else {
      // here also
      return this.bankaccountService.add(this.bankaccount);
    }
  })
  // here you  action following the 'update' or 'add' 
  // (what you wanted to do nested in the previous block)
  .subscribe(bankaccount => {
     this.bankaccount = bankaccount;
  });
  const relUrl = this.router.url.includes('edit') ? '../..' : '..';
  this.router.navigate([relUrl], { relativeTo: this.route });
});

Second, enforce the order of your operations

This was the actual cause of your issue as per your last comments : you have a asynchronous operations order problem.

You want to navigate only after create / edit operation returns . Currently, there is a race condition between the edit and the get.

You are calling the update/create before the select (which makes sense), but since they are asynchronous, sometimes the select operation returns before the update/create has been executed, hence the symptoms you witnessed. (sometimes receiving the old version, even though the update was always executed)

Solution : Call your 'read' operation only after the 'edit/create' has finished.

Hopefully, the fix is very simple here : just move the router.navigate part in the subscription callback, since this part is executed after the operation has returned :

  .subscribe(bankaccount => {
     this.bankaccount = bankaccount;
     const relUrl = this.router.url.includes('edit') ? '../..' : '..';
     this.router.navigate([relUrl], { relativeTo: this.route });
  });
Pac0
  • 21,465
  • 8
  • 65
  • 74
  • also, please try `switchMap` in case `flatMap` doesn't work – Pac0 Mar 14 '18 at 16:01
  • Both do not compile: "Property 'flatMap' doest not exist on type 'Observable'". – Mfried Mar 14 '18 at 16:58
  • @Mfried If so, you are missing an import statement for those operators. you should be able to use something like : `import 'rxjs/add/operator/flatMap';` (same with `switchMap`) – Pac0 Mar 14 '18 at 17:01
  • sorry about that. But with the import same result. – Mfried Mar 14 '18 at 17:20
  • I removed the variable subscription completely and replaced it with `const id = this.route.snapshot.params['id'];` to avoid an Observable in another Observable. However, same result. Strange. I as well thought it could be that the select of the BankaccountListComponent is faster than the update in BankaccountEditComponent. However, according to the log on the server the select is after the update. – Mfried Mar 14 '18 at 17:49
  • @Mfried I didn't really give enough attention to the part after the subscribtion. The "select" is caused by the router.navigate, right ? Then is you need that this is called *after* that the edit returns, then you need to execute the routing it in the subscribe callback. If you keep my code as it is, and just move the two last lines with the router *into* the `subscribe` block, does it work better ? This should make sure that they are called only after edit has returned. Otherwise you have what is called a "race condition", and you depend on luck that the edit returns before the select – Pac0 Mar 14 '18 at 23:28
  • the fact that the select is *called* before the return is not enough, you have to wait that the select *returns* before the select is called. – Pac0 Mar 14 '18 at 23:32
  • The problem was solved by your comment that the routing should be done _inside_ the subscribe block of BankaccountEditComponent#saveBankaccount and not after. Thanks very much! – Mfried Mar 17 '18 at 08:21