ReactiveX/rxjs

`distinctUntilChanged` compare function always has initial value as `previous` param, instead of the previously emitted value

bene-we opened this issue · 6 comments

Describe the bug

When using distinctUntilChanged with a custom compare function on an observable emitting an array, the previous value is always the initial value of the subject, instead of the previous one.

Please have a look on the minimal reproduction example and the console output. Am I getting something wrong on the compare function with distinctUntilChanged?

Actual behavior

[]
0 1
0 2
0 3
      // Missing emit of array

Expected behavior

[]
0 1
1 2
2 3
3 3
["test1", "test2", "test3"]

Reproduction code

import { distinctUntilChanged } from "rxjs/operators";
import { BehaviorSubject } from "rxjs";

const subject = new BehaviorSubject([]);

const test$ = subject.asObservable().pipe(
  distinctUntilChanged((prev, curr) => {
    console.log(prev.length, curr.length);
    return prev.length !== curr.length;
  })
);

test$.subscribe(console.log);

subject.next(["test"]);

subject.next(["test1", "test2"]);

subject.next(["test1", "test2", "test3"]);

subject.next(["test1", "test2", "test3"]);

Reproduction URL

https://codesandbox.io/s/wonderful-thunder-6usy6z?file=/src/index.ts

Version

7.5.7

Environment

Please see codesandbox example

Additional context

No response

https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/distinctUntilChanged.ts#L162-L163

// If it's the first value, we always emit it.
// Otherwise, we compare this key to the previous key, and
// if the comparer returns false, we emit.

Since example in the code's comparator always returns true, it is considered as same value so observable won't emit subsequent values. In result, initial [] is considered as last-emitted value and keep comes as prev.

@kwonoj thanks for your response! Am I correct in the assumption that distinctUntilChanged only uses values that have been emitted completely (past the distinctUntilChanged)?

@bene-we I think you are correct. I've got the same issue and... I dont think it makes sense.

Here we need some way to test if a switch has been changed, i.e. isSaving from true to false. Since it seems to use the after value instead of the real input, it swallows real states for me.

I also created an example: (https://codesandbox.io/embed/rxjs-playground-forked-x8xriv?fontsize=14&hidenavigation=1&theme=dark)

@bene-we just if you're interested here my solution with a separate property in the component:

    private previousValue;

    output$ = this.incoming$.pipe(
      filter(currentValue => {
          if (!this.previousValue) {
             this.previousValue = currentValue; // set it
             return true; // initial should always go through
          }
          const shouldContinue = this.previousValue.id !== currentValue.id; // do your custom comparison

          this.previousValue = currentValue; // obviously has to come after the comparison

          return shouldContinue; 
      }),
   );

This will look at the incoming value instead of the outgoing one.

@bene-we sorry to bother you :) I did some refactoring by creating an operator for this which compares against the last incoming value. Its reusable, no additional property is needed and you can focus on the comparer instead of that additional code from here above inside the filter.

If interested see here: #7180

@daniel-seitz sorry pal for not getting back to you, I’m traveling right now. I will try to test your custom implementation once I’m back. Thanks for your persistent efforts on this!