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.

Upvotes: 1

Views: 178

Answers (1)

Barremian
Barremian

Reputation: 31105

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() {
}

Upvotes: 2

Related Questions