vdshb
vdshb

Reputation: 1999

How to destroy reactive FormControl on Component destroy?

I have a component with FormControl and subscription on it's value change:

@Component(...)
export class FooComponent implements OnInit, OnDestroy {
  fooFormControl: FormControl;

  ...

  ngOnInit() {
    this.fooFormControl.valueChanges.subscribe(
      () => {...},
      () => {},
      () => {
        // never happens
      },
    );
  }

  ngOnDestroy() {
    //happens
  }
}

But when component is destroyed FormControl element isn't destroyed and onComplete callback never happens.

What is the right way to destroy FormControl element on component destroy?

Upvotes: 4

Views: 14542

Answers (4)

Flavien Volken
Flavien Volken

Reputation: 21359

Using takeUntilDestroyed

export class FooComponent implements OnInit {
    destroyRef: DestroyRef = inject(DestroyRef);
    fooFormControl: FormControl; //assuming you do have a valid instance
    
    ngOnInit() {
        this.initOnFooControlChange();
    }
    
    private initOnFooControlChange(){
        this.fooFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(
            () => {},
            () => {},
            () => {},
        );
    }
}

Upvotes: 0

bygrace
bygrace

Reputation: 5988

Here are two ideas that may help you. Not sure if they are the "right way" but it has worked pretty well for me so far. Besides this I am unsubscribing in ngOnDestroy like usual.

#1 Force completion by introducing your own subject via takeUntil

In TypeScript the valueChanges property is of type Observable. If you don't want to work around that and get to the Subject then you can introduce your own Subject by using the takeUntil operator. This will allow you to force the propagation of completion at a level above the valueChanges observable. It is important to not put the takeUntil operator before a switchmap or the switched to observable will continue and your subscription will not be canceled! For that reason I put it right before the subscribe operator.

Here is an example:

const stop = new Subject();

// simulate ngOnDestroy
window.setTimeout(() => {
  stop.next();
  stop.complete();
}, 3500);

Observable
  .interval(1000)
  .takeUntil(stop)
  .subscribe(
    () => { console.log('next'); }, 
    () => { console.log('error'); }, 
    () => { console.log('complete'); }
  );

Here is a working example: https://rxviz.com/v/RoQNBnOM

#2 add the FormBuilder to your component's provider list

This is what I usually do. Actually I usually encapsulate my form inside a service that uses the FormBuilder but the effect is the same. Providing the service at this level will destroy and recreate the service each time the component is created. I started doing this because I kept having weird bugs caused by observable streams that were created off of valueChanges persisting past the lifespan of the component. They would get resubscribed to when the component was recreated and stuff like that.

Here is an example:

@Component({
    selector: 'my-form',
    templateUrl: './my-form.component.html',
    styleUrls: ['./my-form.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [ FormBuilder ] // <-- THIS PART
})
export class MyFormComponent implements OnInit, OnDestroy {
}

Note that this doesn't actually propagate completion. But it does give you a fresh source subject each time.

Upvotes: 2

Estus Flask
Estus Flask

Reputation: 223318

FormControl observables aren't supposed to be completed, although it is possible to do this manually if there is some logic that belongs to complete callback.

Since valueChanges is event emitter and inherits from RxJS Subject, it can be unsubscribed or completed:

ngOnDestroy() {
  this.fooFormControl.valueChanges.complete();  
  // and/or
  this.fooFormControl.valueChanges.unsubscribe();
}

It is possible that EventEmitter won't be a subject in future. This may cause breaking changes, but currently it solely relies on RxJS.

Upvotes: 2

Suren Srapyan
Suren Srapyan

Reputation: 68685

You can't destroy your form control. Everything inside the component will be destroyed with it, but the events can holds in your app. So you can unsubscribe it's events.

@Component(...)
export class FooComponent implements OnInit, OnDestroy {
  fooFormControl: FormControl;
  var subscriber;

  ...

  ngOnInit() {
    this.subscriber = this.fooFormControl.valueChanges.subscribe(
      () => {...},
      () => {},
      () => {
        // never happens
      },
    );
  }

  ngOnDestroy() {
    this.subscriber.unsubscribe();
  }
}

Upvotes: 0

Related Questions