2

I'm still a bit new to rxjs in Angular 5 and it a bit hard to formulate my question. Still I hope for some tips.

I often end up with the same setup:

  • Multiple Components to display the same data
  • A single Service to access the data

Now I have 2 options when receiving data via Observables:

a) Subscribe to an observable to get data once, and subscribe again to get updates

b) Subscribe to an observable and always get updates when data changes

a) ist straight forward, but with b) I often have troubles and wondering if this is the right way to use Observables.

One issue is, that unsubscribe gets important in some cases, and missing the unsubscribe leads to serious garbage being executed with each update of the observable.

On the other hand, with option a) I might miss some updates in one component when another component is updating the underlying data.

Is there any best practice to avoid all these pitfalls?

Tarion
  • 16,283
  • 13
  • 71
  • 107

3 Answers3

2

It sounds like the concept you are trying to figure out is how to regulate subscription management for RxJS when using Angular. There are three main options that come to mind for this:

  1. Automatically create and delete subscriptions using the async pipe. If you want to make UI changes based strictly on data emitted from an observable, then the async pipe handily creates a subscription to the given observable when the component is created and removes those subscription when the component is destroyed. This is arguably the cleanest way to use subscriptions.

As an example:

@Component({
    selector: 'my-component',
    template: `
        <div *ngFor="let value of value$ | async">
            {{value}}
        </div>
    `
})
export class MyComponent {
    public value$: Observable<String> = this.myValueService
        .getValues()
        .map(value => `Value: $value`);
    constructor(myValueService: MyValueService) {}
}
  1. Manage subscriptions in components by creating class-level Subscription objects in the ngOnInit method and then unsubscribing in the ngOnDestroy method. This is the convention that I tend toward when I need access to the subscriptions within the component code. Having ngOnInit and ngOnDestroy methods in every component that uses subscriptions adds boilerplate but is generally necessary if you need subscriptions in your component code.

For example:

@Component({
    selector: 'my-component',
    template: `
        <div #myDiv></div>
    `
})
export class MyComponent implements OnInit, OnDestroy {
    private mySub: Subscription;
    constructor(myValueService: MyValueService) {}

    public ngOnInit() {
        this.mySub = this.myValueService.getValue().subscribe((value) => {
            console.log(value);
            // Do something with value
        });
    }
    public ngOnDestroy() {
        this.mySub.unsubscribe();
    }
}
  1. Limit subscription life by using a limiting operation, such as first(). This is what is done by default when you initiate a subscription to HttpClient observables. This has the benefit of requiring little code, but it can also lead to cases where the subscription is not cleaned up (e.g., if the observable never emits).

If everything that I want to do with an observable can be done in the view, then I virtually always use option 1. This covers most cases in my experience. You can always use intermediate observables to produce an observable that you can subscribe to in the view if you need to. Intermediate observables don't introduce memory leak concerns.

Mike Hill
  • 3,622
  • 23
  • 27
1

Another option is to use the observable to retrieve the data, then let Angular's change detection handle the rest. With Angular's change detection, it will update the UI as the data changes ... no need to subscribe again to get updates.

For example, I have this type of UI: enter image description here

I retrieve the data using Http and an observable. But then I leverage Angular's change detection to handle any updates.

Here is a piece of my service:

@Injectable()
export class MovieService {
    private moviesUrl = 'api/movies';
    private movies: IMovie[];

    currentMovie: IMovie | null;

    constructor(private http: HttpClient) { }

    getMovies(): Observable<IMovie[]> {
        if (this.movies) {
            return of(this.movies);
        }
        return this.http.get<IMovie[]>(this.moviesUrl)
                        .pipe(
                            tap(data => console.log(JSON.stringify(data))),
                            tap(data => this.movies = data),
                            catchError(this.handleError)
                        );
    }

    // more stuff here
}

And here is the full code (except the imports) for the detail component shown on the right above:

export class MovieDetailComponent implements OnInit {
    pageTitle: string = 'Movie Detail';
    errorMessage: string;

    get movie(): IMovie | null {
        return this.movieService.currentMovie;
    }

    constructor(private movieService: MovieService) {
    }

    ngOnInit(): void {
    }
}

You can see the complete example (with editing) here: https://github.com/DeborahK/MovieHunter-communication/tree/master/MH-Take5

DeborahK
  • 57,520
  • 12
  • 104
  • 129
  • The question is, how you detect that "currentMovie" changed? You cache the HTTP result which is fine, but you have to set `curentMovie` to `null` whenever your application changes the cached movie on the server. Or would you do all the changes on the cached instance and than do the HTTP call to update it on the server? – Tarion Mar 18 '18 at 18:25
  • Angular has built-in change detection. So if the value changes in the client-side service, Angular's change detection picks it up and asks any component that might be bound to its data to rebind. The rebinding calls the getter ... which then gets the instance of the new movie. (I don't understand what you mean about updating it on the server? All of this is client-side.) – DeborahK Mar 19 '18 at 06:00
  • I think it would be more clear when you try to extend your example to also update e.g. the Title of the current movie in a second component. How will the `MovieDetailComponent` detect the change in the title to reload the movie? – Tarion Mar 19 '18 at 08:28
  • I have the complete example (with editing) in my github. (Way too much code to try to just paste here.) I added the link to the answer above. – DeborahK Mar 19 '18 at 15:36
1

When passing data between components, I find the RxJS BehaviorSubject very useful.

You can also use a regular RxJS Subject for sharing data via a service, but here’s why I prefer a BehaviorSubject.

  1. It will always return the current value on subscription - there is no need to call onnext().
  2. It has a getValue() function to extract the last value as raw data.
  3. It ensures that the component always receives the most recent data.
  4. you can get an observable from behavior subject using the asobservable() method on behavior subject.
  5. Refer this for more

Example

In a service, we will create a private BehaviorSubject that will hold the current value of the message. We define a currentMessage variable to handle this data stream as an observable that will be used by other components. Lastly, we create the function that calls next on the BehaviorSubject to change its value.

The parent, child, and sibling components all receive the same treatment. We inject the DataService in the components, then subscribe to the currentMessage observable and set its value equal to the message variable.

Now if we create a function in any one of these components that changes the value of the message. The updated value is automatically broadcasted to all other components.

shared.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

@Injectable()
export class SharedService {

  private messageSource = new BehaviorSubject<string>("default message");
  currentMessage = this.messageSource.asObservable();

  constructor() { }

  changeMessage(message: string) {
    this.messageSource.next(message)
  }

}

parent.component.ts

import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";

@Component({
  selector: 'app-sibling',
  template: `
    {{message}}
    <button (click)="newMessage()">New Message</button>
  `,
  styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit {

  message: string;

  constructor(private service: sharedService) { }

  ngOnInit() {
    this.service.currentMessage.subscribe(message => this.message = message)
  }

  newMessage() {
    this.service.changeMessage("Hello from Sibling")
  }

}

sibling.component.ts

import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";

@Component({
  selector: 'app-sibling',
  template: `
    {{message}}
    <button (click)="newMessage()">New Message</button>
  `,
  styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit {

  message: string;

  constructor(private service: SharedService) { }

  ngOnInit() {
    this.service.currentMessage.subscribe(message => this.message = message)
  }

  newMessage() {
    this.service.changeMessage("Hello from Sibling");
  }

}
Vikas
  • 11,859
  • 7
  • 45
  • 69
  • I've been there, but how do you extend this when the `currentMessage` is retrieved from a server and can be changed by other components in your app. – Tarion Mar 18 '18 at 18:28