I would like to know if my code could create memory leaks?
Context
I have a component class that should display 'Applications' objects. It has filtering and pagination capacities.
I created a method loadAppsData() in which I subscribe to an Observable returned after a request to a web service.
This method is called at initialization time, ngOnInit(), or after the user interact with the filtering input field or the paginator (see method onUserInteractionsWithTree() )
My question
To avoid memory leaks I already use
.pipe(takeUntil(this.ngUnsubscribe))
and
ngOnDestroy(): void {
this.ngUnsubscribe.next(); // Unsubscribe from observables.
this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
}
But it seems to me that I create new Subscription object each time I send a request to the server, when I call the subscribe() method. Can that create memory leaks? Should I try to reuse subscriptions object?
Thanks in advance for your help,
Below the Typescript code of my component
import {Component, OnInit, ViewChild, ElementRef, OnDestroy} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';
import {MatPaginator} from '@angular/material';
import {DynamicFlatNode} from './dynamic-flat-node';
import {ApplicationService} from '../shared/service/application-service';
import {DataRequestOptions} from '../../shared/data/data-request-options';
import {MetaDescriptor} from '../../shared/data/meta/meta-descriptor';
import {TableDataRequestParamsService} from '../../shared/data/table-data-request-params.service';
import {ApplicationTreeDatabase} from './application-tree-database';
import {ApplicationTreeDatasource} from './application-tree-datasource';
// Observable classes and extensions.
import {BehaviorSubject, Subject, fromEvent, of, merge} from 'rxjs';
// Observable operators.
import {debounceTime, distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators';
@Component({
selector: 'app-application-tree',
templateUrl: './application-tree.component.html',
styleUrls: ['./application-tree.component.css'],
providers: [ApplicationTreeDatabase]
})
export class ApplicationTreeComponent implements OnInit, OnDestroy {
@ViewChild('appfilter') inputfilter: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
readonly defaultPaginatorPageIndex = 0;
readonly defaultPaginatorPageSize = 2;
readonly defaultPaginatorPageRange = this.defaultPaginatorPageIndex + '-' + (this.defaultPaginatorPageSize - 1);
private ngUnsubscribe: Subject<void> = new Subject<void>();
// Application name filter. START
_inputFilterChange = new BehaviorSubject('');
get inputFilterValue(): string {
return this._inputFilterChange.value;
}
set inputFilterValue(inputFilterValue: string) {
this._inputFilterChange.next(inputFilterValue);
}
// Application name filter. END
treeControl: FlatTreeControl<DynamicFlatNode>;
dataSource: ApplicationTreeDatasource;
getLevel = (node: DynamicFlatNode) => node.level;
isExpandable = (node: DynamicFlatNode) => node.expandable;
hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;
constructor(
private applicationService: ApplicationService,
private dataRequestHelper: TableDataRequestParamsService,
private database: ApplicationTreeDatabase) {
this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new ApplicationTreeDatasource(this.treeControl, this.paginator, database);
}
ngOnInit(): void {
fromEvent(this.inputfilter.nativeElement, 'keyup').pipe(
debounceTime(150)
, distinctUntilChanged()
, switchMap(term => of(term))
, takeUntil(this.ngUnsubscribe)
)
.subscribe(() => {
if (!this.dataSource) {
return;
}
// this.resetPaginator();
this.inputFilterValue = this.inputfilter.nativeElement.value;
});
this.loadAppsData();
this.onUserInteractionsWithTree();
}
ngOnDestroy(): void {
this.ngUnsubscribe.next(); // Unsubscribe from observables.
this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
}
resetFilterAndTriggerChange() {
// Clear HTML filter content.
this.inputfilter.nativeElement.value = '';
// Clear filter data stream. => This will trigger database.load()
// because of Event emmited by inputFilterValueChange.
this.inputFilterValue = '';
}
buildAppDataRequestParams(): DataRequestOptions {
let range = this.dataRequestHelper.buildRequestRangeValue(this.paginator);
if (!range) { // paginator not initialized.
range = this.defaultPaginatorPageRange;
}
return new DataRequestOptions(this.inputFilterValue, 'name', range);
}
private loadAppsData() {
this.applicationService.getDataObjects(this.buildAppDataRequestParams())
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe(dataAndMeta => {
// Update local Apps database.
this.database.updateApplicationData(dataAndMeta.data);
this.updatePaginator(dataAndMeta.meta);
// Inform datasource that data has changed.
this.dataSource.data = this.database.getAppsAsRootLevelNodes();
},
error => {
const errMsg = 'Echec d\'acces aux données';
throw new Error(errMsg);
}
);
}
private onUserInteractionsWithTree() {
const treeUserActionsListener = [
this._inputFilterChange,
this.paginator.page
];
// Merge the array of Observable inputs of treeUserActionsListener
// and put into the source property of a newly created Observable.
const mergeOfObservables = merge(...treeUserActionsListener);
// Create new Observable<RoleMemberClient[]> by calling the function defined below.
mergeOfObservables
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((data: any) => {
this.loadAppsData();
});
}
private updatePaginator(meta: MetaDescriptor) {
if ((meta) && (meta.isPaginatedData)) {
const contentRange = meta.contentRange;
const rangeStart = contentRange.rangeStart;
this.paginator.pageIndex = Math.floor(rangeStart / this.paginator.pageSize);
this.paginator.length = contentRange.size;
} else if (meta) {
// All data can be contained within the first table page.
this.paginator.length = meta.count;
if (this.paginator.pageIndex * this.paginator.pageSize < meta.count) {
// If last requested page do not contain data, do not reset table page index.
// The user will do it by itself.
// Otherwise reset the table page index to zero.
this.paginator.pageIndex = 0;
}
}
}
}