Angular variable is not updating when I return to previous page

Hello, I’m doing the Angular’s Tour of Heroes, but I have a problem with the last topic. When it is necessary to obtain a data from a simulated server, edit a hero’s name and return
to the previous page, my hero continues to display the same name on that page. To resolve this I need to reload the page or navigate to another page and return so that the method for updating the variables is triggered again.

I tried to use the IonViewWillEnter() method together with the function of loading users to try to force this loading the moment I return to the page, but it still didn’t work.

Using Ionic 5 and Angular 10.

hero.service.ts

updateHero(hero:Hero):Observable<any>{
    return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
      tap(_=>this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>('updateHero'))
    );
}

getHeroes(): Observable<Hero[]>{
  	//Send the message after fetching the heroes this.messageService.add('HeroService: fetched heroes');

  	return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
        tap(_ => this.log('fetched heroes')),
        catchError(this.handleError<Hero[]>('getHeroes', []))
      );
  }

hero.detail.component.ts

hero:Hero;

  ngOnInit() {
  	this.getHero();
  }

  getHero(): void{
  	const id = +this.route.snapshot.paramMap.get('id');
  	this.heroService.getHero(id).subscribe(hero => this.hero = hero);
  }

  goBack(): void{
  	this.location.back();
  }

  save(): void{
    this.heroService.updateHero(this.hero)
    .subscribe(() => this.goBack());
  }

hero.detail.component.html

<div *ngIf="hero">

  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
    
    <button (click)="goBack()">go back</button>
    <button (click)="save()">Save</button>
  </div>

</div>

Here, is where the variable needs to appear updated

dashboard.component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
	<a *ngFor="let hero of heroes" class="col-1-4" routerLink ="/detail/{{hero.id}}">
		<div class= "module hero">
			<h4>{{hero.name}}</h4>
		</div>
	</a>
</div>

dashboard.component.ts

heroes:Hero[] = [];


  ngOnInit() {
  	this.getHeroes();
  } 
  
  getHeroes():void{
  	this.heroService.getHeroes().subscribe(heroes =>this.heroes = heroes.slice(1,5));
  }
1 Like

Lifecycle events shouldn’t be responsible for managing data freshness.

And the code does not prove it is actually an ionic app so that specific ionic hook may not be triggered at all

Either way, the best way is to follow the previous post by @rapropos

Thanks for the link provided, I think I understood the differences and how you solved the problem, I found it strange that in the tutorial they don’t approach it that way. I even wonder if this problem is intentional, by some incomplete way of the tutorial, or if was my fault beacause some mistake I made. I ended up using the lifecycle hook as a last measure, I know that Angular alone manages change detection, but even that didn’t work. I tried to make some changes using your method but it also didn’t work:

hero.service.ts

public heroes = new BehaviorSubject<Hero[] | null>(null);

getHeroes(): Observable<Hero[]>{
 	return this.http.get<Hero[]>(this.heroesUrl)
    .pipe(
        tap(res=>{ 
         this.log('fetched heroes');
         this.heroes.next(res); 
        }),
       catchError(this.handleError<Hero[]>('getHeroes', []))
    );
}

watchHeroes(): Observable<Hero[]> { return this.heroes; }
 updateHero(hero:Hero):Observable<any>{
   return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
     tap(res =>{
       this.log(`updated hero id=${hero.id}`)
       this.getHeroes();
     }),
     catchError(this.handleError<any>('updateHero'))
    );
}

hero-detail.component.ts

hero:Hero;
  
constructor(
	private route: ActivatedRoute,
	private heroService: HeroService,
	private location: Location
	) { }

ngOnInit() {
	this.getHero();
}

getHero(): void{
	const id = +this.route.snapshot.paramMap.get('id');
	this.heroService.getHero(id).subscribe(hero => this.hero = hero);
}

goBack(): void{
	this.location.back();
}

save(): void{
  this.heroService.updateHero(this.hero)
  .subscribe(() => this.goBack());
}

dashboard.component.ts

heroes$:Observable<Hero[]> = new Observable<Hero[]>();
heroes:Hero[] = [];

ngOnInit() {
  	this.getHeroes();
    this.heroes$.subscribe(res => {
        return this.heroes = res;
    });
} 
 
getHeroes():void{
	this.heroService.getHeroes().subscribe(_ =>this.heroes$ = this.heroService.watchHeroes());
}

Note: @Tommertom, I don’t know if I understand your question, but I’m using ionic 5 and I adapted the tutorial solution for the app.

First off, and I realize that many people roll their eyes when I talk about this, but I think naming things is super-important. Methods named getFoo() should always return some sort of Foo. Consequently, every call to a function named getFoo() that ignores the return value should strike you as weird.

You wrote a getHeroes method, but HeroDetailComponent seems to be calling getHero. I don’t know what to make of that.

You are leaking subscriptions. It’s important to draw a distinction between “one-shot” Observables and longer-lived ones. My position is that clients (generally speaking, pages and components) should consider any Observable exposed by a service as long-lived, and therefore must unsubscribe on destruction. I use and recommend ngneat/until-destroy for this purpose.

The structure of HeroDetailComponent assumes too much about its lifecycle. I recommend forgetting snapshot of ActivatedRoute exists, because it encourages this flawed design. Instead, fully embrace Rx:

ngOnInit() {
  this.route.params.pipe(
    untilDestroyed(this),
    map(params => params.id),
    switchMap(id => this.heroes.getHero(id)),
  ).subscribe(hero => this.hero = hero);
}

Now, the lifecycle of the Hero we care about is decoupled from the lifecycle of our component. When a new hero id comes in to the route, its id is extracted and turned into a hero by the HeroService.

The tap operator is problematic. I recommend avoiding it entirely until you’re really fluent in Rx, because it is a really tempting crutch that encourages clinging to imperative thinking modes. Ideally, reactive programming shares much with functional programming, and the prime goal of functional thinking is “avoid modifying external state”. tap is explicitly designed to modify external state.

Which brings me to arguably the most subtle bug you have here - one that would be avoided by design if you follow these other guidelines laid out so far.

updateHero(hero:Hero):Observable<any>{
   return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
     tap(res =>{
       this.log(`updated hero id=${hero.id}`)
       // HERE BE DRAGONS
       this.getHeroes();
     }),
     catchError(this.handleError<any>('updateHero'))
    );
}

getHeroes returns an Observable. You ignore it. Most specifically, you don’t subscribe to it. And in the forest of HttpClient, if an HTTP request is built, but nobody subscribes to its Observable, the tree never actually falls - the request will never go out over the network.

2 Likes

Thanks again for the help. About the names I just used the same from the tutorial, but it’s a valid point, it will be meaningfull for future readers of this topic, and I will sure improve my function names in the other projects.

To handle the subscription leak I choose to use the unsubscribe() method on ngOnDestruction(), I believe it will reach the same result, right?

About the tap operator, I don´t know nor use much in my projects, I basically used now beacuse of the Tour of Heroes tutorial, but thanks for the notice, I will need study a lot of Rx.

So now my problem about the update is resolved, here is what I did:

hero.service.ts

private heroes$ = new Subject<Hero[]>();

getHeroes(): Observable<Hero[]>{
	return this.http.get<Hero[]>(this.heroesUrl)
  .pipe(
    tap(res =>{
      this.log('fetched heroes');
      this.heroes$.next(res);
    }),
    catchError(this.handleError<Hero[]>('getHeroes', []))
  );
}

updateHero(hero:Hero):Observable<any>{
  return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
    tap(res =>{
      this.log(`updated hero id=${hero.id}`);
      this.getHeroes().subscribe(heroes => this.heroes$.next(heroes));
    }),
    catchError(this.handleError<any>('updateHero'))
  );
}

getHero$():Observable<Hero[]>{
  return this.heroes$;
}

dashboard.component.ts

private getHeroes$Subscription: Subscription;
private getHeroesSubscription: Subscription;

ngOnInit() {
  this.getHeroes$Subscription = this.heroService.getHero$().subscribe(heroes => {
    console.log('Heroes changed: ', heroes)
    this.heroes = heroes.slice(1,5)
  })
  
  this.getHeroesSubscription = this.heroService.getHeroes().subscribe();  
} 

ngOnDestroy(){
  this.getHeroes$Subscription.unsubscribe();
  this.getHeroesSubscription.unsubscribe();
}
1 Like