uglycode
uglycode

Reputation: 3082

Angular - reusing observable within a component?

So, this is my use case: I get an observable from rest API, then by using async within the template I foreach the data. On click, I get the ID of particular user and then fill-in the form with the data of that user. To achieve the latter, I reuse the existing observable and filter the data, then subscribe to it. I was wondering if my approach is correct, since I believe that app is too slow when I click "edit" for the form to get filled, so I'm guessing here that I create too many subscriptions or whatnot?

Also, for the sake of argument, I created two observables (user and users) and one (user) gets a "reference" to another (users), then it is displayed in the template with async, while I also subscribe to it and set it to a "regular" variable. Is this an issue? To display an observable with async and also subscribing to it?

Here's the code, since my question might be a bit confusing:

//our root app component
import {Component, NgModule, VERSION, OnInit, OnDestroy} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { Observable }     from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/find';
import 'rxjs/add/operator/takeUntil';

import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';

import { UserService } from './service';

@Component({
  selector: 'my-app',
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      <div>
        {{user | async | json}}
        {{selected | json}}
      </div>
      <div *ngFor="let u of users | async">
        {{u.first_name}} {{u.last_name}}
        <br />
        <img [src]="u.avatar" />
        <br />
        <br />
        <a (click)="edit(u.id)">Edit</a>
        <br />
        <br />
      </div>
      <div>
        <form [formGroup]="userForm" (ngSubmit)="onSubmit()" novalidate>
            <input type="text" formControlName="first_name" />
            <input type="text" formControlName="last_name" />
        </form>
        <p>userForm value: {{ userForm.value | json}}</p>
        <p>validity: {{ userForm.valid }}</p>
      </div>
    </div>
  `,
  styles: ['a { cursor: pointer; }']
})
export class App implements OnInit, OnDestroy {
  users: Observable<any>;
  user: Observable<any>;
  destroy$: Subject<boolean> = new Subject<boolean>();

  name:string;
  userForm: FormGroup;

  constructor(private fb: FormBuilder, private service: UserService) {
    this.name = `Angular! v${VERSION.full}`;
    this.createForm();
  }

  createForm() {
    this.userForm = this.fb.group({
      first_name: ['', {updateOn: 'blur', validators: [Validators.required, Validators.minLength(2), Validators.maxLength(10)]}],
      last_name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(20)]]
    })
  }

  edit(id) {
    this.user =  this.users.map(x => x.find(x => x.id === +id));

    this.user.takeUntil(this.destroy$).subscribe(u => {
      console.log(u);
      this.selected = u;

      this.userForm.patchValue({
        first_name: u.first_name,
        last_name: u.last_name
      });

    });
  }

  ngOnInit() {
    console.clear();
    console.log('hello world');

    this.users = this.service.all();
  }

  ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.unsubscribe();
  }
}

@NgModule({
  imports: [ BrowserModule, HttpClientModule, ReactiveFormsModule ],
  providers: [UserService],
  declarations: [ App ],
  bootstrap: [ App ]
})
export class AppModule {}

and here's the link: http://plnkr.co/edit/F0dheIdgoNNw53hNVjZm?p=preview

Upvotes: 0

Views: 1389

Answers (2)

Martin Nuc
Martin Nuc

Reputation: 5764

Let's say you want to use observables for this task. Although there is more straightforward method in the answer from Leon but for sake of learning about Observables let's do it :-)

You are subscribing to user observable in the template using async pipe but also in edit method. The subscription in edit method is never unsubscribed so there is a memory leak.

Also you redefine this.user observable each time you click on user and letting async pipe in template to resubscribe. This results in another memory leak since async pipe has no clue about the overriden observable which should be unsubscribed.

This is not really how you should compose your observable streams. First you should define the observable data transformation.

When I look on your application you have two sources of data/events:

  1. users from service
  2. clicks on edit button

Users are just observable from a service. Clicks should be really Subject. Because on edit you should emit the userId which was clicked (observable) and react on this click by displaying user detail (observer behaviour).

editClick: Subject<number> = new Subject<number>;

So let's define how user observable should look like (all this code in ngOnInit):

this.users = this.service.all().shareReplay();
this.user = this.editClick
    .switchMapTo(this.users, (userId, users) => ({ userId, users}))
    .map(x => x.users.find(user => user.id === +x.userId))
    .do((u) => {
        console.log(u);
        this.selected = u;

        this.userForm.patchValue({
            first_name: u.first_name,
            last_name: u.last_name
      });
   });

Here I define user observable based on editClick - thats where I will get id from later. Using switchMapTo I combine users observable and id from editClick into object which I use later to filter out clicked user.

Now how to trigger editClick in onEdit method? Just emit userId on each click like this:

edit(id) {
    this.editClick.next(id);
}

Notice how I define data stream in ngOnInit. Just once at beginning. No recreation or something. I have two observables to subscribe:

  1. users to display users list
  2. user to display detail - this one will emit each time I trigger editClick

Also there is only subscription in template therefore no memory leaks since async pipe will manage unsubscribe for you.

Upvotes: 1

Leon Radley
Leon Radley

Reputation: 7672

You can greatly simplify the edit method by passing the whole user instead of just the id.

<a (click)="edit(u)">Edit</a>

edit(user) {
  this.selected = user;

  this.userForm.patchValue({
    first_name: user.first_name,
    last_name: user.last_name
  });
}

As for calling service.all() multiple times will cause multiple subscriptions and multiple calls to the backend.

If you want to call it multiple times use the .share() rxjs operator. That will create an wrapper subject around the observable which will return the same value each time it is called.

Upvotes: 0

Related Questions