- Introduction
- What is distinctUntilChanged
- Usage Examples
- Conclusion
- Further reading
- Special thanks to
Introduction
In this post, we’ll have an in-depth look at RxJS’s distinctUntilChanged
operator, it’s signature and what it does, it’s parameters compare
and keySelector
, and typical use cases for each of them and both of them.
What is distinctUntilChanged?
It is an RxJS operator which returns an Observable that emits items from the source Observable with distinct values. It only emits when the current emitted value is different from the last emitted value.
From the official documentation, it
Returns an Observable that emits all items emitted by the source Observable that are distinct by comparison from the previous item.
If a comparator function is provided, then it will be called for each item to test for whether or not that value should be emitted.
The comparator function should return true if the values are the same, and false if they are different.
If a comparator function is not provided, an equality check is used by default.
It’s function signature(s)
A function signature defines input and output of functions or methods, and can include parameters and their types.
Two signatures are presented in the RxJS official docs for distinctUntilChanged
.
-
distinctUntilChanged<T>(compare?: (x: T, y: T) => boolean): Observable<T>;
When this signature is used,
distinctUntilChanged
can be passed an optionalcompare
function. Here, the genericT
represents the type of the valuedistinctUntilChanged
is operating on. -
distinctUntilChanged<T, K>(compare: (x: K, y: K) => boolean, keySelector: (x: T) => K): Observable<T>;
When this signature is used,
distinctUntilChanged
can be passed an optionalcompare
function as well as an optionalkeySelector
function. Here, the genericT
is still the type of the Observable whileK
is the return type of thekeySelector
function.
Let’s have a deeper look at the compare
and keySelector
functions.
The compare
function
It’s an optional comparison function called to test if an item is distinct from the previous item in the source. It is called with two parameters - the current item from the source and the previous item from the source . It should return a boolean
.
If a compare function is not provided, distinctUntilChanged
defaults to it’s built in compare
function which makes use of an equality check.
private compare(x: any, y: any): boolean {
return x === y;
}
Below are a few use cases for a custom compare
function.
- You need to write custom comparison logic, e.g. converting all strings to lowercase before comparing them.
- You are working with objects and would like to compare a specific key in the objects
The keySelector
function
It’s an optional function to select which value you want to check as distinct, a function to compute the comparison key for each element.
If one is passed to distinctUntilChanged
, it’ll be called for every value emitted from the source and it’s result used in making comparisons in the compare
function. It is called with one parameter - the current item from the source.
keySelector
is what you need when working with objects and would like to select a specific key for use in comparisons.
Usage Examples
With value streams e.g numbers, strings
import { of } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
of(1, 1, 2, 2, 2, 1, 1, 2, 3, 3, 4).pipe(
distinctUntilChanged(),
)
.subscribe(x => console.log(x)); // 1, 2, 1, 2, 3, 4
With value streams and the compare
function
import { of } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
/**
* Create a stream from a mix of upper and lowercase characters
*/
of('a', 'A', 'b', 'c', 'D', 'd', 'e', 'f', 'G', 'g', 'h').pipe(
distinctUntilChanged((curr, prev) => {
/**
* Two characters are the same if they match each other in lowercase
*/
return curr.toLowerCase() === prev.toLowerCase();
}),
)
.subscribe(x => console.log); // a b c D e f G h
With objects streams
Note that using distinctUntilChanged on an object stream will result in the comparison of object references and not the objects’ property values.
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const sampleObject = { name: 'Test' };
// Objects must be same reference
const source$ = from([sampleObject, sampleObject, sampleObject]);
// only emit distinct objects, based on last emitted value
source$
.pipe(
distinctUntilChanged()
)
.subscribe(console.log);
// console output:
// {name: 'Test'}
Since all items in source$
array reference the same object, sampleObject
, only one item gets outputted to the console after applying distinctUntilChanged
.
Should we change the reference of one of the objects in source$
array, the output will change as follows.
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const sampleObject = { name: 'Test' };
// Change the reference of the second array item
const source$ = from([sampleObject, {...sampleObject}, sampleObject]);
// only emit distinct objects, based on last emitted value
source$
.pipe(
distinctUntilChanged()
)
.subscribe(console.log);
// console output:
// {name: 'Test'}
// {name: 'Test'}
// {name: 'Test'}
When the reference of the second item from the source$
observable is changed, it results in all the items being outputted to the console. This is because the first item has a unique reference, the second item has a unique reference, and the third item doesn’t have a unique reference, but it has a reference different from that of the second item.
With object streams and the compare
function
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
/**
* Create a stream from an array of car objects.
*/
from([
{ name: 'Porsche', model: '911' },
{ name: 'Porsche', model: '911' },
{ name: 'Ferrari', model: 'F40' }
]).pipe(
distinctUntilChanged((prevCar, nextCar) => {
/**
* Two cars are the same if they have the same name and model
*/
return (prevCar.name === nextCar.name)
&& (prevCar.model === nextCar.model);
})
)
.subscribe(console.log);
// console output:
// {name: "Porsche", model: "911"}
// {name: "Ferrari", model: "F40"}
With object streams and the keySelector
function
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
/**
* Create a stream from an array of car objects.
*/
from([
{ name: 'Porsche', model: '911' },
{ name: 'Porsche', model: '911' },
{ name: 'Ferrari', model: 'F40' }
]).pipe(
/**
* Use keySelectorFn to return car names for use in comparison
*/
distinctUntilChanged(null, (car) => car.name)
)
.subscribe(console.log);
// console output:
// {name: "Porsche", model: "911"}
// {name: "Ferrari", model: "F40"}
With object streams + the compare
and keySelector
functions
I’ve not been able to come up with or find a convincing enough code example for this use case, if you know/think of one, please let me know so I update the article. Here’s a contrived example.
import { from } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
/**
* Create a stream from an array of car objects with
* names in both lower and upper case.
*/
from([
{ name: 'Porsche', model: '911' },
{ name: 'PORSCHE', model: '911' },
{ name: 'Ferrari', model: 'F40' },
{ name: 'FERRARI', model: 'F40' },
]).pipe(
distinctUntilChanged(
/**
* Convert car names returned by keySelectorFn to lower case
* before doing a comparison.
*/
(prevCarName, nextCarName) => {
return prevCarName.toLowerCase() === nextCarName.toLowerCase();
},
/**
* Use keySelectorFn to return car names for use in comparison
*
* We have to manually add types due to a Typescript bug.
* @see https://github.com/ReactiveX/rxjs/issues/5484
*/
(car: {name: string, model: string}) => car.name
)
)
.subscribe(console.log);
// console output:
// {name: "Porsche", model: "911"}
// {name: "Ferrari", model: "F40"}
Conclusion
In this post, we’ve had an in-depth look at RxJS’s distinctUntilChanged
operator and it’s two optional parameters, compare
and a keySelector
. We equally did an overview of it’s function signature(s), as well as specific usage examples.
Did you like this post? Then you might also enjoy reading my recent post, Creating a Delayed Input Directive in Angular in which I apply distinctUntilChanged
as discussed this post.
Further reading
Special thanks to
for reviewing this post and providing valuable and much-appreciated feedback!