Utilizing a foundational element to automatically unsubscribe from multiple observable subscriptions

Within our Angular application, we have implemented a unique concept using a Base Component to manage observable subscriptions throughout the entire app. When a component subscribes to an observable, it must extend the Base Component. This approach ensures that all subscriptions remain active until the entire application is finally destroyed, rather than being destroyed with each individual component:

base.component.ts:

import { Subject } from 'rxjs';
import { OnDestroy, Component } from '@angular/core';

export abstract class BaseComponent implements OnDestroy {
  protected unsubscribe$ = new Subject<void>();

  ngOnDestroy(): void {
    this.unsubscribe$.next();
    this.unsubscribe$.complete();
  }
}

the-rest-of-our-components.ts:

import { Component, OnInit } from '@angular/core';
import { MyService } from 'src/app/services/my.service';
import { BaseComponent } from '../base/component/base-component';

export class myComponent extends BaseComponent implements OnInit {
  myProperty: string;

  constructor(
    private myService: MyService,
  ) {
    super();
  }
  ngOnInit(): void {
    this.myService.doStuff$
      .pipe(takeUntil(this.unsubscribe$)) // take until baseComponent's unsubscribe$
      .subscribe((data) => {
        this.myProperty = data;
      });
  }


If multiple components extend BaseComponent and make use of its unsubscribe$ Subject, do all the subscriptions only get unsubscribed when the entire application is closed (when Base Component is destroyed), as opposed to when individual components are destroyed?

Is this an effective strategy you've encountered before? Is it recommended? If my assumptions hold true, it means that all subscriptions in our application will persist until the entire app is terminated. Depending on our requirements, this could be beneficial or detrimental.

Bonus question: Does Base Component act like a singleton? In other words, if multiple components simultaneously extend BaseComponent, do they share the same instance of unsubscribe$, or does each component have its own separate instance?

Answer №1

I decided to put my assumption to the test rather than relying on it, as we all know where assumptions can lead us: https://stackblitz.com/edit/angular-ivy-ueshwz?file=src/app/extended/extended.component.ts

Through experimentation, I found that subscriptions are indeed destroyed when individual components are destroyed.


To demonstrate this, I created a service with a subject for subscription and a value that can be modified through the subscription:

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

@Injectable({ providedIn: 'root' })
export class UpdateService {
  subject = new Subject<void>();
  value = 0;
}

In the main component, I continuously trigger the subject every second and have a toggleable subcomponent:

export class AppComponent implements OnInit {
  extCompOpen = true;

  constructor(public update: UpdateService) {}

  ngOnInit() {
    interval(1000).subscribe(() => this.update.subject.next());
  }
}
<app-extended *ngIf="extCompOpen"></app-extended>
<button (click)="extCompOpen = !extCompOpen">Toggle Component</button>

<p>The counter will increment as long as the subscription is active:</p>
<p>{{ update.value }}</p>

Furthermore, an extended subcomponent increments the value by 1 using the subscription:

export class ExtendedComponent extends BaseComponent implements OnInit {
  constructor(private update: UpdateService) {
    super();
  }

  ngOnInit() {
    this.update.subject.pipe(takeUntil(this.unsubscribe$)).subscribe(() => {
      this.update.value++;
    });
  }
}
<p>Extended component functionality confirmed!</p>

Interestingly, closing the subcomponent halts the incrementation process, indicating that the subscription has been successfully unsubscribed.


A bonus question arises regarding BaseComponent's behavior as a singleton - creating individual instances does not result in shared parent class instances. Extending a class simply adds properties and methods to the specific instance.


While this approach may work, caution is advised since overriding ngOnDestroy() requires calling super.ngOnDestroy(), which could easily be overlooked. With only four lines of code, manually including it in each component might be a safer practice. Manual subscriptions should ideally be infrequent, especially when utilizing the async pipe.

Answer №2

A solution I implemented in a recent project involved the following steps:

Inside the base.component file:

private subscriptions: any = {};

ngOnDestroy() {
  Object.keys(this.subscriptions).map(subscriptionKey => {
     this.subscriptions[subscriptionKey].unsubscribe();
  })
}

Then, in any component that extends this base component:

this.subscriptions.myService = this.myService.doStuff$.subscribe(......

Using this approach ensures that the subscription remains active until the component is destroyed.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I trigger the rendering of a component by clicking a button in a separate component?

I am currently facing a challenge in rendering a component with an "Edit" button that is nested within the component. I attempted to use conditional rendering in my code, but unfortunately, I am struggling to make it work as expected. Does anyone have any ...

A simple method for bulk editing in Angular UI Grid

Is there a way to enable mass editing in Angular UI Grid by allowing all rows to show editable input fields at once, rather than just one at a time? I have searched online for a solution without success and am now turning to this forum for help. If anyone ...

The functionality of enabling and disabling dynamic behavior in AngularJs is not functioning as anticipated

As a newcomer to AngularJS, I may have some basic questions. I am currently working on implementing dynamic behavior for a button click event, but it's not functioning as expected. Could this be due to an issue with scope? Below is my HTML code: < ...

Getting the checkbox count value in a treeview upon successful completion of an AJAX request

After successful JSON Ajax response, a treeview structure with checkboxes is displayed. I need to capture the number of checkboxes checked when the save button is clicked. @model MedeilMVC_CLOUD.Models.UserView <script type="text/javascript"> ...

Error: The React Material-UI modal is encountering a type error where it cannot access the property 'hasOwnProperty' of an undefined value

Whenever I include a modal in one of my classes, I encounter this error. Error: Unable to access property 'hasOwnProperty' of undefined Here is a simple example where I am attempting to display a basic modal at all times. Any suggestions? I h ...

How to build an angular2/typescript project without including node_modules in team foundation 2013?

I am facing an issue with my angular2 application where it works fine locally but fails to build on the build server. I want to avoid checking in the massive number of files (29,000) within the node_modules directory as part of our build process (local, te ...

Updating website content dynamically using Javascript and JSON-encoded data

My programming code seems to be acting oddly. I have structured my data in a JSON object as follows: injectJson = { "title": "Champion Challenge Questions", "rules": [ { "idChrono": "chrono-minute", "text": "Top is missing!", ...

Error: The provided `anchorEl` property for this component is invalid

While working on my React 18.2 app with MUI 5.10.5, I encountered an issue trying to create a <Menu /> element that should open when a button is clicked. The menu does appear, but the positioning seems off as it displays in the top-left corner of the ...

Implementing ExpressJS with MongoDB on a MERN Development Stack

After configuring my ExpressJS & MongoDB client and running Nodemon, I consistently encounter the following warning: "DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version. To use the ...

Tips on aligning three divs horizontally within a container while maintaining a height of 100%

UPDATE: The alignment has been corrected by adding floats. However, the height still doesn't fill 100%. Check out the new look: Image Link In my footer container, I want to arrange 3 columns (colored green, white, and red for clarity). Currently, the ...

The issue of basic authentication failing to function on Internet Explorer and Chrome, yet operating successfully on Firefox

Here is how my authentication code looks: public class BasicAuthenticationMessageHandler : DelegatingHandler { private const string BasicAuthResponseHeader = "WWW-Authenticate"; private const string BasicAuthResponseHeaderValue = "Basi ...

Methods for reloading the requirejs module

There are two modules: settingmap.js var settingMap = { scWidth : [4000, 6000, 8000], scHeight : [5000, 7000, 9000], bxWidth : [100, 90, 80], bxHeight : [100, 90, 80], totalTime : [50, 40, 30], level : [1, 2, 3], boxColor : [&a ...

Leveraging an intersection type that encompasses a portion of the union

Question: I am currently facing an issue with my function prop that accepts a parameter of type TypeA | TypeB. The problem arises when I try to pass in a function that takes a parameter of type Type C & Type D, where the intersection should include al ...

Clicking a link will trigger a page refresh and the loading of a new div

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script> $(window).load(function () { var divs = $( #add_new, #details"); $("li a").click(function () { ...

Learn the process of sending a delete request to a REST API with Angular

Is there a way to effectively send a delete request to a REST API using Angular? I am attempting to send a delete request with an ID of 1 My current approach is as follows: this.http.delete(environment.apiUrl+"id="+1).subscribe(data => { }); The va ...

When you make a POST request to an express API, the properties are not clearly defined

Just getting into Vue.JS and experimenting with creating a basic MEVN to-do list app for practice. I keep encountering an issue when trying to send a POST request to my express server, receiving the error message: TypeError: Cannot read properties of unde ...

Sass: Setting a maximum width relative to the parent element's width

I have a resizable container with two buttons inside, one of which has dynamic text. Within my scss file, I am aiming to implement a condition where if the width of the container is less than 200, then the max width of the dynamic button should be 135px, ...

The guidelines specified in the root `.eslintrc.json` file of an NX workspace do not carry over to the project-level `.eslintrc.json` file

In my main .eslintrc.json file, I have set up some rules. This file contains: { "root": true, "ignorePatterns": ["**/*"], "plugins": ["@nrwl/nx", "react", "@typescript-eslint", &qu ...

Preventing AngularJS from Ignoring HTML Elements

Is there a way to calculate HTML content within an AngularJS object (such as {{names}}) that includes an '<a>' element? When I try to display it, the result looks like this: <a href="http://www.example.com">link text</a> I&ap ...

The Angular test spy is failing to be invoked

Having trouble setting up my Angular test correctly. The issue seems to be with my spy not functioning as expected. I'm new to Angular and still learning how to write tests. This is for my first Angular app using the latest version of CLI 7.x, which i ...