Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 - FormGroup ValueChanges Unsubscribe

I have a FormGroup with ValueChanges event that is not being released from memory when the user moves from component's route to another component and then they return to the component.

What this means is that if the user navigates away from component and then back to the component 5 times, the onFormChange method fires 5 times, but only 1 of those calls is for the current component.

I figured that the problem was that I needed to unsubscribe from the the valueChanges event in the NgDestroy event, but there is no unsubscribe method available on the valueChanges event.

I am sure I have to unsubscribe or release memory for something, but I'm not sure what.

import * as _ from 'lodash';
import {Observable} from 'rxjs/Rx';

import {Component, Input, Output, EventEmitter, OnInit, OnDestroy} from '@angular/core';
import {FormGroup} from '@angular/forms';

import {formConfig} from './toolbar.form-config';
import {JobToolbarVm} from '../view-model/job-toolbar.vm';
import {BroadcastService} from '../../../services/broadcast/broadcast.service';

@Component({
    selector: 'wk-job-toolbar',
    template: require('./toolbar.html'),
})
export class JobToolbarComponent implements OnInit, OnDestroy {

  protected form: FormGroup;

  @Input()
  toolbar: JobToolbarVm;

  @Output()
  toolbarChanged = new EventEmitter<JobToolbarVm>();

  @Output()
  refresh = new EventEmitter<string>();

  constructor(private broadcast: BroadcastService) {
  }

  ngOnInit() {

    this.form = formConfig;
    this.form.setValue(this.toolbar, {onlySelf: true});

    // This ALWAYS RUNS when the form loads, ie. on the job route
    console.log('FORM VALUE');
    console.log(JSON.stringify(this.form.value, null, 2));

    this.form.valueChanges
      .debounceTime(2000)
      .subscribe(
        this.onFormChange.bind(this)
      );
  }

  ngOnDestroy() {
    //this.form.valueChanges.unsubscribe();
    //this.onChanges.unsubscribe();
    //this.toolbarChanged.unsubscribe();
    //this.form = null;
  }

  onFormChange(data: any) {
    // This runs whenever I go to a different route and then come back to this route
    // There is also a memory leak, because this method fires multiple times based on how
    // often I navigate away and come back to this route.
    // e.g. Navigate away and then back 5 times, then I see this log statement 5 times 
    console.log('FORM VALUE2 - THIS KEEPS FIRING FOR EACH INSTANCE OF MY COMPOMENT');
    console.log(JSON.stringify(this.form.value, null, 2));

    JobToolbarVm.fromJsonIntoInstance(data, this.toolbar);

    this.onChanges('data-changed');
  }

  onChanges($event: any) {
    console.log('onChanges: ' + $event);
    // console.log(this.toolbar);

    // Send the toolbar object back out to the parent
    this.toolbarChanged.emit(this.toolbar);

    // Broadcast an event that will be listened to by the list component so that it knows when to refresh the list
    this.broadcast.broadcast('job-admin-toolbar-changed', this.toolbar);
  }
}
like image 591
David Cruwys Avatar asked May 08 '17 06:05

David Cruwys


4 Answers

The subscribe() call returns a Subscription and this is what you use to unsubscribe:

class JobToolbarComponent

  private subscr:Subscription;

  ngOnInit() {
    ...
    this.subscr = this.form.valueChanges ...
    ...
  }

  ngOnDestroy() {
    this.subscr.unsubscribe();
  }
}
like image 146
Günter Zöchbauer Avatar answered Nov 15 '22 10:11

Günter Zöchbauer


I have created this following function

export function AutoUnsubscribe(exclude = []) {

    return function (constructor) {

        const original = constructor.prototype.ngOnDestroy;

        constructor.prototype.ngOnDestroy = function () {
            for (let prop in this) {
                const property = this[prop];
                if (!exclude.includes(prop)) {
                    if (property && (typeof property.unsubscribe === "function")) {
                        property.unsubscribe();
                    }
                }
            }
            original && typeof original === 'function' && original.apply(this, arguments);
        };
    }

}

which actually you can use to auto unsubscribe all the watchers but you have to store them in public properties so that this function can intercept it and invoke unsubscribe on that. How you use it is mentioned below:-

@AutoUnsubscribe()
@Component({
    selector: 'account-login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {


    public submitWatcher: Subscription;

     submit() {
        this.submitWatcher = this.authService.login(this.loginForm.getRawValue())
            .subscribe(res => {
                if (this.returnUrl) {
                    this.router.navigate([this.returnUrl]);
                }
                else {
                    this.router.navigate(['/special']);
                }
            }, (error) => {
                alert(JSON.stringify(error.data));
            });
    }

}

For more info on how to use decorator please read this blog that's where I have taken the idea from and it is pretty cool

Blog

like image 22
Atul Chaudhary Avatar answered Nov 15 '22 08:11

Atul Chaudhary


You could do the following:

// private variable to hold all your subscriptions for the component
private subscriptions: Subscription[] = [];

// when you subscribe to an observable,
// you can push all subscription this.subscriptions

this.subscriptions.push(
       this.form.valueChanges.pipe(
            .debounceTime(2000)) 
            .subscribe(val => this.onFormChange.bind(this)),

       this.observe2$.subscribe(val => this.somemethod(val))
    );

// in ngondestroy
    ngOnDestroy(): void {
        if (this.subscriptions && this.subscriptions.length > 0) {
            this.subscriptions.forEach(s => s.unsubscribe());
        }
    }

like image 4
techie net20 Avatar answered Nov 15 '22 10:11

techie net20


The easiest to put together the subscriptions is to use the add method : subscription.add( anotherSubscription ) so now you just have to unsubscribe from only one subscription.

You can find the following example from the official doc : https://rxjs.dev/guide/subscription

import { interval } from 'rxjs';
 
const observable1 = interval(400);
const observable2 = interval(300);
 
const subscription = observable1.subscribe(x => console.log('first: ' + x));
const childSubscription = observable2.subscribe(x => console.log('second: ' + x));
 
subscription.add(childSubscription);
 
setTimeout(() => {
  // Unsubscribes BOTH subscription and childSubscription
  subscription.unsubscribe();
}, 1000); 
like image 2
Gauthier Peel Avatar answered Nov 15 '22 09:11

Gauthier Peel