Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJS distinctUntilChanged - object comparison

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
            }
        });
    }
}
like image 272
Daniel Suchý Avatar asked May 11 '16 20:05

Daniel Suchý


5 Answers

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))

This code will work if the attriutes are in the same order, if not it may break, so here's a quick fix for that (be careful this method is slower)

.distinctUntilChanged((a, b) => JSON.stringify(a).split('').sort().join('') === JSON.stringify(b).split('').sort().join(''))

like image 187
Mehdi Benmoha Avatar answered Nov 14 '22 12:11

Mehdi Benmoha


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!

like image 22
Daniel Suchý Avatar answered Nov 14 '22 12:11

Daniel Suchý


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),
like image 30
hoeni Avatar answered Nov 14 '22 11:11

hoeni


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

Cavets

This method also has several pitfalls as some commenters have pointed out.

  1. This will fail if the objects fields are ordered differently;
JSON.stringify({a:1, b:2}) === JSON.stringify({b:2, a:1})
// will return false :(
  1. This JSON.stringify will also throw an error if the object has circular references. For example stringify'ing something from the firebase sdk will lead you to these errors:
TypeError: Converting circular structure to JSON

Robust Solution

Use the deep-object-diff library instead of JSON.stringify. This will solve the above problems 👍

import { detailedDiff } from 'deep-object-diff';

function isSame(a, b) {
  const result = detailedDiff(a, b); 
  const areSame = Object.values(result)
    .every((obj) => Object.keys(obj).length === 0);
  return areSame;
}

function distinctUntilChangedObj<T>() {
  return distinctUntilChanged<T>((a, b) => isSame(a, b));
}
like image 14
Ben Winding Avatar answered Nov 14 '22 13:11

Ben Winding


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);
like image 9
Whisher Avatar answered Nov 14 '22 13:11

Whisher