Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composing view models with Observables

Tags:

angular

rxjs

The below code lightly illustrates what I am trying to do with 2 models. Lets say we have a Book model and an Author model. First we fetch a list of Books using a service that returns a type Observable<Book>. We then use *ngFor to iterate through each one, but we want to also display the Author next to the book so in our iteration we have some HTML like this:

<span>{{ getAuthorName(book) | async }}</span>

getAuthorName has its own request go out on the AuthorService to fetch the author by ID. The problem is that when we return an Observable<string> here, the browser completely crashes because change detection runs amok due to the fact that a "new" observable is returned on each digest cycle which causes change detection to run again, endlessly, right?

The below code can easily be put into a StackBlitz and reproduced (I didn't link to it because it blows up your browser CPU)

<ul>
  <li *ngFor="let item of getData() | async">
    {{ getOtherData(item.id) | async }}
  </li>
</ul>
import { Component } from '@angular/core';
import { of } from 'rxjs';
import { filter, map } from 'rxjs/operators';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  name = 'Angular';

  getData() {
    return of([{
      id: 1
    },{
      id: 2
    },{
      id: 3
    }]);
  }

  getOtherData(id: number) {
    const data = of([{
      id: 1,
      name: 'Matt'
    },{
      id: 2,
      name: 'Steve'
    },{
      id: 3,
      name: 'Alice'
    }]);

    return data.pipe(
      map(x => x.find(d => d.id == id))
    )
  }
}

Obviously there are some optimizations like caching of the Authors, etc, that could be done here, but in general, how is this scenario supposed to be handled in the world of RXJS? I know there are entire frameworks for dealing with state like Akita and ngrx, but what if I don't want to deal with all that and just want to build my app with raw rxjs? What is the intended solution to this problem? Do you use some other rxjs operators to stop this from happening? (I couldn't find anythat helped this since it was returning a new Obserable each time anyways) Do you have to only use view models and not methods? That way ther eis always just a single reference to the observable? (e.g. BookAuthorViewModel which has a $author property that gets set before we iterate in the view to a single observable?

I haven't found a single solution that "feels right" and I feel like I am forced to use these state management frameworks just to do simple things like this.

like image 575
Matt Hintzke Avatar asked Mar 26 '26 15:03

Matt Hintzke


2 Answers

For performance reasons you shouldn't use functions in the html template. I recommend to merge the data before using it in the template. This can look like this:

this.books$ = this.myService.getBooks();
this.authors$ = this.myService.getAuthors();

this.bookAuthorPairs$ = combineLatest([this.books$, this.authors$]).pipe(
  map(([books, authors]) => books.map(
    (book) => {
      return { book, author: authors.find(author => book.authorId === author.id) };
    }
  )),
);
like image 183
MoxxiManagarm Avatar answered Mar 29 '26 06:03

MoxxiManagarm


You should not call methods in your view. If you have a method that returns an observable then you get a new observable being created each time change detention checks the value.

data$ = of([{
  id: 1
},{
  id: 2
},{
  id: 3
}]);

otherData$ = of([{
  id: 1,
  name: 'Matt'
},{
  id: 2,
  name: 'Steve'
},{
  id: 3,
  name: 'Alice'
}]);

and then bind to the observable

<li *ngFor="let item of data$ | async">
  {{ otherData$ | async | find: item.id }}
</li>

and make the find pipe

@Pipe({
  name: 'find'
})
export class FindPipe implements PipeTransform {
  transform(options, id): string {
    return options.find(option => option.id === id);
  }
}

StackBlitz https://stackblitz.com/edit/angular-6vgosc

like image 30
Adrian Brand Avatar answered Mar 29 '26 08:03

Adrian Brand