tony09uk
tony09uk

Reputation: 2991

Angular 4 unsubscribe causes an error when navigating

I have recently learnt to avoid memory leak, I need to unsubscribe in my components. To do this I implement OnDestroy but this has caused an issue when navigating.

ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'unsubscribe' of undefined TypeError: Cannot read property 'unsubscribe' of undefined

If I don't unsubscribe navigation completes successfully.

I was under the impression that ngOnDestroy only happens once everything else has completed, obviously that is not the case.

I'm obviously doing something really wrong, but I cant figure out what How do I unsubscribe when navigating away from a page?

export class PropertyFormMapperComponent implements OnInit, OnDestroy {
    private _routeSubscription: any;
    private _getPropertySubscription: any;

    constructor(
        private _route: ActivatedRoute,
        private _propertyFormFileService: PropertyFormFileService,
        private _router: Router
    ) { }

    ngOnInit(): void {
        this._routeSubscription = this._route.params.subscribe(params => {
            this._id = params['id'];
            this._getPropertySubscription = this._propertyService.getProperty(this._id).subscribe(property => {
                ...do stuff...
            });
        });
    }

    ngOnDestroy(): void {
        this._routeSubscription.unsubscribe();
        this._getPropertySubscription.unsubscribe();
    }

    private onCreate(event: any): void {    
        this._propertyService.postProperty(property, 1).subscribe(
            response => {
                ...do stuff...
                this._router.navigate(['home']);
            },
            error => {
                ...other stuff...
            }
        );
    }

}

Upvotes: 2

Views: 1895

Answers (1)

codeepic
codeepic

Reputation: 4122

Here, this one should work for you. I combined route params stream and getProperty stream using concatMap and projection function. You can read more about it in rxjs docs: rxjs concatMap

I also use takeUntil which unsubscribes from the stream it is piped into when other stream emits. rxjs takeUntil

So when componentDestroyed$ observable emits, because takeUntil took it as a param, takeUntil will then unsubscribe from this._route.params and this._propertyService.getProperty streams.

I also added takeUntil to onCreate method, since you also subscribe in there.

takeUntil is an alternative to keeping track of all your Subuscriptions and manually unsubscribing from them in ngOnDestroy. With takeUntil we achieve the same thing, but don't have to clutter the component with subscription properties.

You can take it one step up and have a base class that all your component classes inherit from --> then you don't have to keep componentDestroyed$ in every component and can skip ngOnDestroy() part too:

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

    ngOnDestroy() {
        this.componentDestroyed$.next();
        this.componentDestroyed$.complete();
    }
}

And your components now will be inheriting from BaseComponent:

export class PropertyFormMapperComponent extends BaseComponent implements OnInit, OnDestroy

Your code fixed:

import {Subscription} from 'rxjs';
import {tap, map, concatMap, takeUntil} from 'rxjs/operators';

export class PropertyFormMapperComponent implements OnInit, OnDestroy {
    componentDestroyed$: Subject<void> = new Subject<void>();

    constructor(
        private _route: ActivatedRoute,
        private _propertyFormFileService: PropertyFormFileService,
        private _router: Router
    ) { }

    ngOnInit() {
        this._route.params
            .pipe(
                map(params => params['id']),
                tap(id => this._id = id),
                concatMap(id => this._propertyService.getProperty(id))
                takeUntil(this.componentDestroyed$),
                tap(property => {
                   ...do stuff...
                   ... side effects are handled in tap functions     
                })
            )
            .subscribe();        
    }

    private onCreate(event) {    
        this._propertyService.postProperty(property, 1)
            .pipe(
                takeUntil(this.componentDestroyed$)
            )
            .subscribe(
                response => {
                    ...do stuff...
                    this._router.navigate(['home']);
                },
                error => {
                    ...other stuff...
                }
            );
    }

    ngOnDestroy() {
        this.componentDestroyed$.next();
        this.componentDestroyed$.complete();
    }
}

Upvotes: 1

Related Questions