8

Reading How to use RxJs distinctUntilChanged? and this, it seems that distinctUntilChanged alters the output stream to only provide distinct contiguous values.

I take that to mean that if the same value arrives in immediate succession, you are essentially filtering the stream and only getting one occurrence of that repeated value.

So if I write this:

this.myFormControl.valueChanges
  .debounceTime(1000)
  .distinctUntilChanged()
  .subscribe(newValue => {
    console.log('debounced: ', newValue);
  });

I see no difference in output to this:

this.myFormControl.valueChanges
  .debounceTime(1000)
  .subscribe(newValue => {
    console.log('debounced: ', newValue);
  });

I've seen a few places recommend using distinctUntilChanged when subscribing to valueChanges on form inputs — but don't really get why.

It's an input, so if the user is typing it is always changing, right? The values will always be distinct, so you're just adding an extra operation to filter input for no reason.

Or am I missing something here?

EDIT

Using distinctUntilChanged as per my first code sample, I created a form input with the value Mr Trump and ensured it was saved in the model.

I then clicked inside the control and pasted Mr Trump. Since the value is the same, I would have expected to not see anything logged — the control has the same value it had before, so surely the distinctUntilChanged should have ignored it?

EDIT 2

After further looking into my test, this behaviour appears to be because I used an array of AbstractControls:

this.itemsControl = <FormArray>this.form.controls['items']; 
...
this.itemsControl.controls[index].valueChanges...

So although a bit strange that it still fires when the value of the input is the same, I am guessing I need to hookup to valueChanges of the actual input inside this array item (a form group), and not the array item itself.

EDIT 3

So after changing the code in EDIT 2 to the following, pasting the same value that already exists into input control does not fire valueChanges (as expected). In EDIT 2 valueChanges was hooked to the entire formGroup, not the individual input control (in this case called content):

let fg = this.itemsControl.controls[index]; // get the formGroup
fg['controls'].content.valueChanges
  .debounceTime(1000)
  .distinctUntilChanged()
  .subscribe(newValue => {...});
skink
  • 5,133
  • 6
  • 37
  • 58
rmcsharry
  • 5,363
  • 6
  • 65
  • 108

3 Answers3

26

distinctUntilChanged when applied to the valueChanges observable...

...is probably never going to work!

It only works as expected on an individual value (as you said). So you'll get a new value even if nothing has changed.

To track changes for the whole form you need to write a custom comparer, the simplest of which uses JSON.stringify to output a value that can be compared. The valueChanges observable emits an object and distinctUntilChanges isn't smart enough to do anything beyond a reference compare (that's a link to the RxJS source code) unless you provide a comparer function.

this.form.valueChanges.pipe(distinctUntilChanged((a, b) => JSON.stringify(a) === 
                                                           JSON.stringify(b)))

                      .subscribe(changes => { console.log('The form changed!'); });

distinctUntilChanged works fine for an individual value with a primitive type because === is sufficient to detect changes.

How do I solve infinite loops?

If you're trying to add distinctUntilChanges into your pipe (for the whole form) to avoid an infinite loop when you programmatically update the form value - you probably want this instead:

    this.form.patchValue(address || {}, { emitEvent: false });
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
  • 1
    +1 Thank you for taking the time to clarify this, especially when the answer is already given. It explains what I was seeing in EDIT2 so definitely adds value here. A shame I cannot mark 2 answers as correct! – rmcsharry Dec 18 '18 at 09:21
  • 1
    This is the correct answer - and just to add, I like using lodash - `.pipe(distinctUntilChanged(_.isEqual))` – Adam Jenkins Jan 30 '19 at 00:57
  • 1
    I like the `isEqual` lodash thing :-) I've become a big fan recently of creating my own `RxJS` operators. So now I just added `export const distinctUntilEqualChanged = (): MonoTypeOperatorFunction => pipe(distinctUntilChanged(_.isEqual));` – Simon_Weaver Jan 30 '19 at 06:19
  • 2
    BTW if you're using only a few lodash functions with angular make sure to use the `lodash-es` npm package to avoid importing the entire library. Then you can do `import { isEqual as _isEqual } from 'lodash-es';` – Simon_Weaver Jun 25 '19 at 18:10
13

Using debounceTime(1000) means we only send a request when the user stopped typing for 1 second, during that second the user can write 3 characters then erase them, so the input value didn't change since last request but you are sending the same request, to avoid that you can use .distinctUntilChanged()

  this.myFormControl.valueChanges
      .debounceTime(1000)
      .distinctUntilChanged()
      .subscribe(newValue => {
        console.log('debounced: ', newValue)
      });
Kld
  • 6,970
  • 3
  • 37
  • 50
  • Ah ha, that is kind of obvious now you point it out, thanks for taking the time to do so! :) – rmcsharry Oct 23 '17 at 13:18
  • I am filling in my input my unit cost for a product, with the same logic, but now I can only type 1 number at a time , unless i go back and click on the field. I would like to be able to type in multiple digits at a time which is the normal default. How can I do this – user3701188 Sep 12 '18 at 21:32
0

If you want check if the previous value from the valueChanges of FormControl isn't the same of the next value you can use the operator pairwise:

this.myFormControl.valueChanges
.pipe(
  .debounceTime(1000)
  .pairwise(),
  .tap(([valuePrev, valueNext]: [any, any]) => {
      if(valuePrev !== valueNext){
          console.log(valueNext);
      }
  })
)
.subscribe();

In depth pairwise emit the previous and current values as an array:

// Return a couple of the last two value emitted in an array of 2 values
import { pairwise, take } from 'rxjs/operators';
import { interval } from 'rxjs';

//Returns: [0,1], [1,2], [2,3], [3,4], [4,5]
interval(1000)
  .pipe(pairwise(), take(5))
  .subscribe(console.log);
Alessandro_Russo
  • 1,799
  • 21
  • 34