37
votes

I have a stream of objects and I need to compare if the current object is not the same as the previous and in this case emit a new value. I found distinctUntilChanged operator should do exactly what I want, but for some reason, it never emits value except the first one. If I remove distinctUntilChanged values are emitted normally.

My code:

export class SettingsPage {
    static get parameters() {
        return [[NavController], [UserProvider]];
    }

    constructor(nav, user) {
        this.nav = nav;
        this._user = user;

        this.typeChangeStream = new Subject();
        this.notifications = {};
    }

    ngOnInit() {

        this.typeChangeStream
            .map(x => {console.log('value on way to distinct', x); return x;})
            .distinctUntilChanged(x => JSON.stringify(x))
            .subscribe(settings => {
                console.log('typeChangeStream', settings);
                this._user.setNotificationSettings(settings);
            });
    }

    toggleType() {
        this.typeChangeStream.next({
            "sound": true,
            "vibrate": false,
            "badge": false,
            "types": {
                "newDeals": true,
                "nearDeals": true,
                "tematicDeals": false,
                "infoWarnings": false,
                "expireDeals": true
            }
        });
    }

    emitDifferent() {
        this.typeChangeStream.next({
            "sound": false,
            "vibrate": false,
            "badge": false,
            "types": {
                "newDeals": false,
                "nearDeals": false,
                "tematicDeals": false,
                "infoWarnings": false,
                "expireDeals": false
            }
        });
    }
}
9
Is typeChangeStream an Observable? Hard to tell what's wrong without seeing what code is creating that / etc. - Mark Pieszak - Trilon.io
Please have a look at stackoverflow.com/help/mcve for guidelines to deal with 'it does not work' questions. Basically post a minimally verifiable example that reproduces the error, and post the expected behaviour and how it is different from the current behavior. Talk about JSON stringifying you need to know that it is not a bullet proof method for checking equality of objects. {"a" : 2, "b":1} is for example different from {"b":1, "a":2} while these are the same objects - user3743222
Sorry for that, i added more code. I don't need bullet proof object equality check, i am sure in this case object will be everytime i same order. - Daniel Suchý
did u solve this problem? i have the same with BehaviorSubject, and it emits prev and curr same result, wtf.. - Den Kerny
@DanielSuchý it would be good IMO to accept the answer that did work for you - Mehdi Benmoha

9 Answers

55
votes

I had the same problem, and fixed it with using JSON.stringify to compare the objects:

.distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))

Dirty but working code.

25
votes

I finally figure out where problem is. Problem was in version of RxJS, in V4 and earlier is different parameters order than V5.

RxJS 4:

distinctUntilChanged = function (keyFn, comparer)

RxJS 5:

distinctUntilChanged = function (comparer, keyFn)

In every docs today, you can find V4 parameters order, beware of that!

23
votes

When you have lodash in your application anyway, you can simply utilize lodash's isEqual() function, which does a deep comparison and perfectly matches the signature of distinctUntilChanged():

.distinctUntilChanged(isEqual),

Or if you have _ available (which is not recommended anymore these days):

.distinctUntilChanged(_.isEqual),
11
votes

You can also wrap the original distinctUntilChanged function.

function distinctUntilChangedObj<T>() {
  return distinctUntilChanged<T>((a, b) => JSON.stringify(a) === JSON.stringify(b));
}

This lets you use it just like the original.

$myObservable.pipe(
  distinctUntilChangedObj()
)
9
votes

From RxJS v6+ there is distinctUntilKeyChanged

https://www.learnrxjs.io/operators/filtering/distinctuntilkeychanged.html

const source$ = from([
  { name: 'Brian' },
  { name: 'Joe' },
  { name: 'Joe' },
  { name: 'Sue' }
]);

source$
  // custom compare based on name property
  .pipe(distinctUntilKeyChanged('name'))
  // output: { name: 'Brian }, { name: 'Joe' }, { name: 'Sue' }
  .subscribe(console.log);
3
votes

Mehdi's solution although fast but wouldn't work if the order is not maintained. Using one of deep-equal or fast-deep-equal libraries:

.distinctUntilChanged((a, b) => deepEqual(a, b))
1
votes

The answers suggesting a deep compare all fail if the observable is actually emitting a modified version of the same object, as the "last" value that is passed to the comparison will be the same as the current, every time.

We've hit this using a BehaviorSubject where it just so happens that the same object is passed to .next() each time.

In this case, there is no solution using the default distinctUntilChanged, no matter what comparison function you use.

0
votes

You can also make your own filter:

function compareObjects(a: any, b: any): boolean {
  return JSON.stringify(a) === JSON.stringify(b);
}

export function distinctUntilChangedObject() {
  return function<T>(source: Observable<T>): Observable<T> {
    return new Observable(subscriber => {
      let prev: any;
      let first = true;
      source.subscribe({
        next(value) {
          if (first) {
            prev = value;
            subscriber.next(value);
            first = false;
          } else if (!compareObjects(prev, value)) {
            prev = value;
            subscriber.next(value);
          }
        },
        error(error) {
          subscriber.error(error);
        },
        complete() {
          subscriber.complete();
        }
      });
    });
  };
}
-1
votes

If you change the value mutably, none of the other answers work. If you need to work with mutable data structures, you can use distinctUntilChangedImmutable, which deep copies the previous value and if no comparison function is passed in, will assert that the previous and the current values deep equals each other (not === assertion).