Search code examples
angularrxjs

Why is my RxJS merge subscription firing when no observables have emitted values?


I have Angular code like this which listens to changes on multiple fields and then saves the changes when any of them change. The problem is that the story is getting saved even though nothing is being logged to the console, which means no observables should have fired. Why is the save getting called and how can I debug this?

    merge([
        this.form.get('storyName').valueChanges.pipe(tap(x => console.log('storyName', x))),
        this.form.get('storyTypeId').valueChanges.pipe(tap(x => console.log('storyTypeId', x))),
        this.form.get('backlogId').valueChanges.pipe(tap(x => console.log('backlogId', x))),
        this.form.get('ownerUserId').valueChanges.pipe(tap(x => console.log('ownerUserId', x))),
        this.form.get('points').valueChanges.pipe(tap(x => console.log('points', x))),
        this.form.get('tags').valueChanges.pipe(tap(x => console.log('tags', x))),
      ]).pipe(
        debounceTime(300),
      ).subscribe(async () => {
        try {
          await this.saveStory();
        } finally {
          this.isSavingStory = false;
        }
      });

Solution

  • If think the only problem in your code is that you passed an array to the merge operator instead of declaring the inner obervables one after the other :

    I believe this should work :

    merge(
      this.form.get('storyName').valueChanges.pipe(tap(x => console.log('storyName', x))),
      this.form.get('storyTypeId').valueChanges.pipe(tap(x => console.log('storyTypeId', x))),
      this.form.get('backlogId').valueChanges.pipe(tap(x => console.log('backlogId', x))),
      this.form.get('ownerUserId').valueChanges.pipe(tap(x => console.log('ownerUserId', x))),
      this.form.get('points').valueChanges.pipe(tap(x => console.log('points', x))),
      this.form.get('tags').valueChanges.pipe(tap(x => console.log('tags', x))),
    ).pipe(
      debounceTime(300),
    ).subscribe(async () => {
      try {
        await this.saveStory();
      } finally {
        this.isSavingStory = false;
      }
    });
    

    I'd also like you give you a tip, you can use the controls property from a FormGroup to get a nice autocompletion when trying to access the contained form controls by name :

    merge(
      this.form.controls.storyName.valueChanges.pipe(tap(x => console.log('storyName', x))),
      this.form.controls.storyTypeId.valueChanges.pipe(tap(x => console.log('storyTypeId', x))),
      this.form.controls.backlogId.valueChanges.pipe(tap(x => console.log('backlogId', x))),
      this.form.controls.ownerUserId.valueChanges.pipe(tap(x => console.log('ownerUserId', x))),
      this.form.controls.points.valueChanges.pipe(tap(x => console.log('points', x))),
      this.form.controls.tags.valueChanges.pipe(tap(x => console.log('tags', x))),
    ).pipe(
      debounceTime(300),
    ).subscribe(async () => {
      try {
        await this.saveStory();
      } finally {
        this.isSavingStory = false;
      }
    });
    

    Hope this helps !

    Edit:

    The merge operator can be used to merge observable with non observable values, thus why it accepted the array, but it never tried to subscribe to it.

    A good example of this behaviour would be a simple merge that combine an observable with an array of string :

    merge(
      ['a value', 'another value'], 
      of('hello')
    ).subscribe(console.log);
    

    In this small example, all values in the merge operator will be logged ('a value', 'another value', 'hello')

    You can test this behaviour in this StackBlitz

    https://stackblitz.com/edit/rxjs-eyje1l?file=index.ts