angularrxjsrxjs-subscriptions

Angular 8 do I need to subscribe to a request if I don't care about the response


I hope this makes sense. I have decided to change the way some of my services are working simply because it was becoming a bit cumbersome to subscribe to responses and handle creates, updates and deletes in different views. So I decided to make a generic service like this:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { Resource } from '../models/resource';
import { ToastrService } from 'ngx-toastr';
import { BehaviorSubject } from 'rxjs';

@Injectable({
    providedIn: 'root',
})
export class DataService<T extends Resource> {
    items: BehaviorSubject<T[]>;

    constructor(private endpoint: string, private http: HttpClient, private toastr: ToastrService) {
        this.items = new BehaviorSubject<T[]>([]);
    }

    initialize(feedId: number) {
        return this.http.get<T[]>(`${environment.apiUrl}/feeds/${feedId}/${this.endpoint}`).pipe(
            map(response => {
                console.log(this.endpoint, response);
                this.items.next(response);
                return response;
            }),
        );
    }

    get(id: number) {
        return this.http.get<T>(`${environment.apiUrl}/${this.endpoint}/${id}`);
    }

    create(filter: T) {
        return this.http.post<T>(`${environment.apiUrl}/${this.endpoint}`, filter).pipe(
            map((response: any) => {
                const message = response.message;
                const item = response.model;

                let items = this.items.value;
                items.push(item);

                this.emit(items, message);

                return response.model;
            }),
        );
    }

    update(filter: T) {
        return this.http.put<T>(`${environment.apiUrl}/${this.endpoint}`, filter).pipe(
            map((response: any) => {
                const message = response.message;
                const item = response.model;

                let items = this.items.value;
                this.remove(items, filter.id);
                items.push(item);

                this.emit(items, message);

                return response.model;
            }),
        );
    }

    delete(id: number) {
        return this.http.delete<string>(`${environment.apiUrl}/${this.endpoint}/${id}`).pipe(
            map((response: any) => {
                let items = this.items.value;
                items.forEach((item, i) => {
                    if (item.id !== id) return;
                    items.splice(i, 1);
                });

                this.emit(items, response);

                return response;
            }),
        );
    }

    private remove(items: T[], id: number) {
        items.forEach((item, i) => {
            if (item.id !== id) return;
            items.splice(i, 1);
        });
    }

    private emit(items: T[], message: string) {
        this.items.next(items);
        this.toastr.success(message);
    }
}

The idea behind this service is that the initialize method is called only once, when it has been called, you can see that it maps the response to the items array within the service itself. Then when a create, update or delete is performed, it is that array that is changed.

That would (in theory) allow any component to subscribe to the items array to get updated with any changes.

So, I have a few services that "extend" this service, for example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Filter } from '@models';
import { DataService } from './data.service';
import { ToastrService } from 'ngx-toastr';

@Injectable({
    providedIn: 'root',
})
export class FilterService extends DataService<Filter> {
    constructor(httpClient: HttpClient, toastr: ToastrService) {
        super('filters', httpClient, toastr);
    }
}

So far, so good. So, my question is: Do I have to call the initialize method and invoke a subscription?

For example, currently I have this component:

import { Component, OnInit, Input } from '@angular/core';
import { first } from 'rxjs/operators';

import { FilterService } from '@services';
import { NgAnimateScrollService } from 'ng-animate-scroll';

@Component({
    selector: 'app-feed-filters',
    templateUrl: './filters.component.html',
    styleUrls: ['./filters.component.scss'],
})
export class FiltersComponent implements OnInit {
    @Input() feedId: number;
    displayForm: boolean;

    constructor(private animateScrollService: NgAnimateScrollService, private filterService: FilterService) {}

    ngOnInit() {
        this.initialize();
    }

    navigateToForm() {
        this.displayForm = true;
        this.animateScrollService.scrollToElement('filterSave');
    }

    private initialize(): void {
        this.filterService
            .initialize(this.feedId)
            .pipe(first())
            .subscribe(() => {});
    }
}

As you can see with the private method, I pipe, then first and then subscribe which is what I would do if I want to get the results from there. In my "child" component, I have this:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { first } from 'rxjs/operators';

import { Filter } from '@models';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmationDialogComponent } from '@core';
import { FilterService } from '@services';
import { FiltersSaveComponent } from './filters-save.component';

@Component({
    selector: 'app-filters',
    templateUrl: './filters.component.html',
    styleUrls: ['./filters.component.scss'],
})
export class FiltersComponent implements OnInit {
    filters: Filter[];

    constructor(private modalService: NgbModal, private filterService: FilterService) {}

    ngOnInit() {
        this.filterService.items.subscribe(filters => (this.filters = filters));
    }

    openModal(id: number) {
        const modalRef = this.modalService.open(ConfirmationDialogComponent);
        modalRef.componentInstance.message = 'Deleting a filter is irreversible. Do you wish to continue?';
        modalRef.result.then(
            () => {
                this.filterService.delete(id);
            },
            () => {
                // Do nothing
            },
        );
    }

    openSaveForm(filter: Filter) {
        const modalRef = this.modalService.open(FiltersSaveComponent);
        modalRef.componentInstance.feedId = filter.feedId;
        modalRef.componentInstance.filterId = filter.id;
        modalRef.componentInstance.modal = true;
    }
}

As you can see, I subscribe to the items array from the filterService. So, in my parent controller, I figure I don't actually need the subscription, but if I remove it it doesn't work.

I thought I would be able to do something like:

private initialize(): void {
    this.filterService.initialize(this.feedId);
}

instead of

private initialize(): void {
    this.filterService
        .initialize(this.feedId)
        .pipe(first())
        .subscribe(() => {
            // I don't need this
        });
}

Am I doing something wrong, or is this just the way I have to do it? I hope I explained myself :)


Solution

  • You must call subscribe on any request method on the HttpClient for the request to be sent. The HttpClient returns a cold observable which means it won't run until something subscribes to it (as opposed to a hot observable that starts running immediately).

    Additionally, an observable from the the HttpClient will only ever emit one value, the response, which means piping it to first is unneccessary. Your final logic will look something like this:

    this.filterService.initialize(this.feedId).subscribe(() => undefined);
    

    Alternatively, instead of subscribing where the service is used you can instead subscribe in the DataService and then your call will look like:

    this.filterService.initialize(this.feedId);
    

    A nice thing about the HttpClient is that the observables they return will never emit again so there is no need to keep track of the subscription and close it later.