RxJS distinctUntilChanged - object comparison
Solution 1
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(''))
Solution 2
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),
Solution 3
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!
Solution 4
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.
- 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 :(
- This
JSON.stringify
will also throw an error if the object has circular references. For example stringify'ing something from thefirebase 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));
}
Solution 5
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);
Daniel Suchý
Updated on July 09, 2022Comments
-
Daniel Suchý almost 2 years
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 } }); } }
-
Always_a_learner almost 6 yearsHow did you fix this? What did you change? If you can tell this it would be great.
-
user3701188 over 5 yearsI 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
-
Mehdi Benmoha over 5 yearsHave you tried the throttling or debouncing rxJS operators ?
-
user3701188 over 5 yearsthat was the 1st thing I thought of, to use debounce but what I realised was when I type in the 1st digit the input loses focus then after the time span the values are emitted so it doesnt allow me to finish typing
-
cobolstinks over 5 yearsthanks this save my butt, angular's valuesChanged keeps emitting values even though nothing was changed. This object comparison put an end to that for me.
-
Milad over 5 yearsthis will give you incorrect
true
when youdelete
a property and add it back. -
Mehdi Benmoha over 5 yearsI didn't understand your scenario, JSON.stringify will simply convert the object to string. Even if you don't have the same properties order it won't work !
-
Mehdi Benmoha over 5 yearsThis is why I said that this is a dirty but working code. Because it has a lot of limits but it's fast AF.
-
Nik Handyman over 4 years@Harsimer swap comparer and key function
-
Den Kerny about 4 yearsClass constructor Subscriber cannot be invoked without 'new' at new DistinctUntilChangedSubscriber
-
Frik over 3 yearseven though it works, it messes with the pipeline objects
-
Ben Winding over 3 years@Frik how so?
JSON.stringify()
shouldn't mess/mutate anything right? If you have circular json in your objects, then you might need to use another package which can stringify it properly npmjs.com/package/circular-json -
Frik over 3 yearsyou are correct. However, there seems to be a bug with WebStorm where it does not see the value o in the tap operator... so you don't have code completion for some reason... from([ { name: 'Brian' }, ]).pipe( distinctUntilChangedObj(), tap(o => console.log(o.name)) ).subscribe();
-
Ben Winding over 3 yearsIf you're using typescript, then you could also do
function distinctUntilChangedObj<T>() { return distinctUntilChanged<T>((a, b) => JSON.stringify(a) === JSON.stringify(b)); }
. This will preserve the type correctly, but only if you're using typescript -
Reza over 3 years
JSON.stringify({a:1, b:2}) === JSON.stringify({b:2, a:1})
this is false, while the result should have been ture -
Mehdi Benmoha over 3 years@Reza yes that’s why I said dirty, because it will check properties order. But it solves majority of cases, where you have exact same objects, you can consider this solution like ===
-
pjdicke over 3 yearsNice answer! This will be very useful.
-
pjdicke over 3 yearsFYI, if
name
was an object like{name: {first: 'Brian'}}
this doesn't work as it doesn't compare the the entire objects. -
TamusJRoyce about 2 yearsJust gonna repeat #Reza. Not a fan of "opinion" that this is ok because it is "dirty" - JSON.stringify({a:1, b:2}) === JSON.stringify({b:2, a:1}) this is false, while the result should have been true - Reza
-
TamusJRoyce about 2 yearsJust gonna repeat #Reza. Not a fan of "opinion" that this is ok because it is "dirty" - JSON.stringify({a:1, b:2}) === JSON.stringify({b:2, a:1}) this is false, while the result should have been true - Reza
-
TamusJRoyce about 2 yearsthere is structuredClone polyfil/new standard. Why isn't there structuredEquals replacement for lodash? lodash isn't compiler friendly when it comes to tree shaking
-
Mehdi Benmoha about 2 years@TamusJRoyce using the second option that sorts the string solves this order issue with more costs (it's slower) but in major cases you dont need to sort the object.
-
Ben Winding about 2 yearsThat's true @TamusJRoyce, there's other ways it can fail too, for example circular references in the objects. I've updated answer to address these problems
-
Bobby_Cheh about 2 yearsCan you clarify what you mean in the sentence: "Or if you have _ available (which is not recommended anymore these days):" because you literally just said if you have lodash in your application you can utilize lodash's isEqual() function, but isn't using "_" the way to use lodash to call the isEqual method??
-
hoeni about 2 years@Bobby_Cheh When you import the
_
-Symbol, this includes all of lodash's functionality, whether you use it or not. This cannot be reduced to the really needed ones later to reduce the resulting bundle size in a build process (this is called "Tree Shaking", because all unconnected things fall of :-) ). When you just require the single functions you need, it's only theses that get bound to your final program bundle: e.g.import {isEqual} from 'lodash'
. Now it's tree-shakeable, so all the functions of lodash you didn't use (probably most part of lodash ;-) ) will not be included. -
TamusJRoyce about 2 years@MehdiBenmoha still messes up objects like dates and such. Better to stick with a robust method then serialization. Ben's solution is deep-object-diff. With structuredClone, I am surprised to not see a likewise proper standard deep compare.