O.O.
O.O.

Reputation: 2013

Restructure code to avoid Angular Change Detection Infinite Loop

My model has a one to many relationship between Post and Imgs i.e. each Post has a number of Imgs. One of the simplified ways of displaying this structure would be:

<ul>
   <li *ngFor="let post of posts">
     {{post.postId}} 
      <p  *ngFor="let img of getImgs(post.postId)">{{img.url}}</p>
   </li>
 </ul>

My problem is that the page goes into an Infinite Loop due to Angular's Change Detection mechanism. Is there a better way to restructure this code? (I'm using Angular 5) A simplified version of my code is as below:

My AppComponent looks like:

import { Component } from '@angular/core';
import {Post} from "./post"
import {PostService} from "./post.service";
import {Observable} from "rxjs/Observable";
import {Img} from "./img";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [PostService]
})
export class AppComponent {
  title = 'app';
  posts : Post[];
  constructor(private postService: PostService) { }
  ngOnInit(): void {
    this.getPosts();
  }

  getPosts(): void {
    this.postService.getPosts().subscribe(posts => this.posts = posts);
  }

  getImgs(postId : string) : Img[] {
    var retImgs : Img[];
    this.postService.getImgsByPostId(postId).subscribe( imgs => retImgs = imgs);
    return retImgs;
  }
}

I get Posts and Imgs from a Web Service. The Posts do not contain the Imgs as part of themselves, but the Imgs are retrieved using the postId. If I would like to keep this model where the Imgs are not part of the Posts, I find it difficult to store the Imgs in the AppCompnent. This forces me to put the getImgs() call in the HTML Template, which gets called on every Change Detection event. What is the normal way in Angular 5 to handle this scenario?

I can ofcourse do some hacks to save/cache the output of getImgs(), so I don't need to call the Web Service for subsequent requests or I could just change my model, but I am wondering how this is normally done.

Is there anyway to call a method from the Template such that it does not get called on every change detection mechanism?


Edit In Response to @Floors suggestion to use a smarter getImgs() that would cache the Images in a Map I have tried the following AppComponent: (The Imports and the Component Decorator are as above)

export class AppComponent {
  title = 'app';
  posts : Post[];
  postIdToImgs : Map<string, Img[]>;  //Added
  constructor(private postService: PostService) {
    this.postIdToImgs = new Map<string, Img[]>();  //Added
  }
  ngOnInit(): void {
    this.getPosts();
  }

  getPosts(): void {
    this.postService.getPosts().subscribe(posts => this.posts = posts);
  }

  getImgs(postId : string) : Img[] {
    var retImgs : Img[];
    console.log(`Call to getImgs(${postId})`);  //Added
    if(this.postIdToImgs.has(postId)) {    //Added
      console.log('Found value in Cache');  //Added
      retImgs = this.postIdToImgs.get(postId);  //Added
      return retImgs; //Added
    }  //Added
    this.postService.getImgsByPostId(postId).subscribe( imgs => {
    this.postIdToImgs.set(postId, imgs); //Added
    retImgs = imgs;
});
    return retImgs;
  }
}

Yes, this stops calls to the Backend after the first iteration, but I still get an Infinite sequence of

Cal to getImgs(#postId)
Found value in Cache

Where #postId is one of the 10 posts that I have on this page. I'm trying to learn Angular, so I'm not trying to just get this to work. I'm trying to find out:

Is there a way to have a method/function in the Template that is not called on every change detection?

Upvotes: 1

Views: 1657

Answers (2)

David
David

Reputation: 34435

You can move your code to a pipe, which is not called everytime change detection takes place

https://angular.io/guide/pipes#pure-pipes

@Pipe({
  name: 'getImages',

})
export class GetImagesPipe  implements PipeTransform {
   postIdToImgs : Map<string, Img[]> =  new Map<string, Img[]>();

  constructor(private postService: PostService) { }

  transform(postId: string): any {


       var retImgs : Img[];
    console.log(`IN PIPE: Call to getImgs(${postId})`); 

    /*This commented cache implementation is not needed as the pipe won't be called again if the post does not change

      if(this.postIdToImgs.has(postId)) {   
      console.log('IN PIPE: Found value in Cache'); 
      retImgs = this.postIdToImgs.get(postId);  
      return retImgs; 
    } */ 
    this.postService.getImgsByPostId(postId).subscribe( imgs => {
    this.postIdToImgs.set(postId, imgs); //Added
    retImgs = imgs;
});
    return retImgs;
  }


}

I created a stackblitz https://stackblitz.com/edit/angular-9n9h18?file=app%2Fimg.pipe.ts

Upvotes: 2

user6749601
user6749601

Reputation:

The best way to solve this problem is to put both object into a wrapper. Fill this wrapper with both the post and it's belonging images. And then perform ngFor using this wrapper.

e.g.

import { Component } from '@angular/core';
import {Post} from "./post"
import {PostService} from "./post.service";
import {Observable} from "rxjs/Observable";
import {Img} from "./img";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [PostService]
})
export class AppComponent {
  title = 'app';
  myWrapperList: Array<MyWrapper> = []; 

  constructor(private postService: PostService) { }
  ngOnInit(): void {
    this.init();
  }

  init(): void {

    this.postService.getPosts().subscribe( posts => 

       posts.forEach(post => {
           this.postService.getImgsByPostId(post.postId).subscribe(imgs => 
             let myWrapper = new MyWrapper(post, imgs);
             this.myWrapperList.push(myWrapper);
       });

    );
  }

}


export class MyWrapper {
    constructor(
      public post: Post,
      public images: Array<Images>
    ){}
}

And in your template

<ul>
   <li *ngFor="let wrapper of myWrapperList">
     {{wrapper?.post?.postId}} 
      <p  *ngFor="let img of wrapper.images">{{img?.url}}</p>
   </li>
 </ul>

Upvotes: 2

Related Questions