Search code examples
angularrxjsobservablebehaviorsubject

Angular 7 rxjs BehavioralSubject emiting repeated values


I'm still learning rxjs and run into a problem.

I have a service with a BehavioralSubject intended to hold a single value and emmiting it to other components when changed. The other components will change the value so they comunciate bettween components- I am using it with a component that does an http request to save a document when it recieves a specific value from the subscription (another component is in charge of changing that value). Once I start the application it works fine, but the second time it emits the value 2 times, sending 2 http requests, 3 the 3rd time, 4 the 4th, and so on...

Here is the code for the service

save.service.ts

export class SaveService {

    readonly state: BehaviorSubject<SAVE_STATE>;

    constructor() {
        this.state = new BehaviorSubject(SAVE_STATE.IDLE);
    }

    public save() {
        this.state.next(SAVE_STATE.SAVE);
    }

    public reset() {
        this.state.next(SAVE_STATE.RESET);
    }

    public changed() {
        this.state.next(SAVE_STATE.CHANGED);
    }

    public idle() {
        this.state.next(SAVE_STATE.IDLE);
    }

    public loading() {
        this.state.next(SAVE_STATE.LOADING);
    }

}

Here is the component that changes the value

save-options.component.ts

    private show: boolean;
    private loading: boolean;

    constructor(private saveService: SaveService) { }

    ngOnInit() {
        this.show = false;
        this.saveService.state.subscribe((state) => {
            this.show = state === SAVE_STATE.IDLE ? false : true;
            this.loading = state === SAVE_STATE.LOADING ? true : false;
        });
    }

    saveAction() {
        this.saveService.save();
    }
    discardAction() {
        this.saveService.reset();
    }

Here is the function in the component that recives the value and makes the request, this method is called in the ngOnInit()

create-or-edit.component.ts

    private listenToSaveEvents() {
        this.saveService.state.subscribe((state) => {
            console.log(state);
            switch(state){
                case SAVE_STATE.SAVE:
                    this.saveStore();
                    break;
                case SAVE_STATE.RESET:
                    this.undo();
                    break;
                default:
                    break;
            }
        });
    }

The later function is the one executing multiple times incrementally. the result of the log is: First execution

0
3
4
3

Second execution

0
3
4
3

0
3
(2)4
(2)3

I might be using BehaviorSubject wrong but can't manage to figure out why, thank you.


Solution

  • Probably the create-or-edit.component.ts component is created and destroyed multiple times. As a general rule, it is always safe to unsubscribe in the ngonDestroy() hook to avoid memory leaks.

    Option 1

    You could try to unsubscribe the subscription in the ngDestroy() hook. Try the following

    create-or-edit.component.ts

    stateSubscription: any;
    
    private listenToSaveEvents() {
      this.stateSubscription = this.saveService.state.subscribe((state) => {
        console.log(state);
        switch(state) {
          case SAVE_STATE.SAVE:
            this.saveStore();
            break;
          case SAVE_STATE.RESET:
            this.undo();
            break;
          default:
            break;
        }
      });
    }
    
    ngOnDestroy() {
      if (this.stateSubscription) {
        this.stateSubscription.unsubscribe();
      }
    }
    

    Option 2

    It might get difficult to keep track of all the subscriptions and unsubscribe in the ngOnDestroy hook. You could use this solution to workaround it.

    Setup the following shared function:

    // From https://stackoverflow.com/a/45709120/6513921
    // Based on https://www.npmjs.com/package/ng2-rx-componentdestroyed
    
    import { OnDestroy } from '@angular/core';
    import { ReplaySubject } from 'rxjs';
    
    export function componentDestroyed(component: OnDestroy) {
      const oldNgOnDestroy = component.ngOnDestroy;
      const destroyed$ = new ReplaySubject<void>(1);
      component.ngOnDestroy = () => {
        oldNgOnDestroy.apply(component);
        destroyed$.next(undefined);
        destroyed$.complete();
      };
      return destroyed$.asObservable();
    }
    

    Now all there is to do is to implement ngOnDestroy in the component and add takeUntil(componentDestroyed(this)) to the pipe.

    import { pipe } from 'rxjs';
    import { takeUntil } from 'rxjs/operators';
    
    private listenToSaveEvents() {
      this.stateSubscription = this.saveService.state
        .pipe(takeUntil(componentDestroyed(this)))          // <-- pipe it in here
        .subscribe((state) => {
          console.log(state);
          switch(state) {
            case SAVE_STATE.SAVE:
              this.saveStore();
              break;
            case SAVE_STATE.RESET:
              this.undo();
              break;
            default:
              break;
          }
        });
    }
    
    ngOnDestroy() {
    }