RxJS distinctUntilChanged: filtering out duplicate emissions
The RxJS distinctUntilChanged
operator is a filter operator, it filters out duplicate emissions. It compares each emission with the previous and only emits the new value if it is different from the previous value.
By default, it uses ===
(strict equality) to compare the new and old value, but you can also provide a custom comparison function.
In this blog post we’ll cover:
- Basic usage of
distinctUntilChanged
- Provinding a custom comparison function
- Real-world use cases with
distinctUntilChanged
in Angular - Conclusion
Basic usage of distinctUntilChanged
The distinctUntilChanged
operator is often used to optimize our streams. It will only emit a new value if it is different to the previous value.
In this example, we can see how we optimized the alphabet$
Observable by using distinctUntilChanged
:
// Import the distinctUntilChanged operator
import { distinctUntilChanged } from 'rxjs/operators';
// Observable that emits a, b, b, c, c, c, d, d, d, d
const alphabet$ = of('a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd');
// Without optimization of distinctUntilChanged
alphabet$.subscribe(console.log);
// Output: a, b, b, c, c, c, d, d, d, d
// With optimization of distinctUntilChanged
alphabet$.pipe(distinctUntilChanged()).subscribe(console.log);
// Output: a, b, c, d
Providing a custom comparison function
By default, the distinctUntilChanged
operator uses ===
(strict equality) to compare the new and old value.
But you can also provide a custom comparison function to compare the new and old value.
In the example below, we have an Observable users$
that emits users one by one. We want to filter out subsequent duplicate emissions based on their name.
type User = { id: string; name: string; };
const users$ = of(
{ id: '1', name: 'John' },
{ id: '2', name: 'John' }, // will be filtered out
{ id: '3', name: 'John' }, // will be filtered out
{ id: '4', name: 'Jane' },
{ id: '5', name: 'Jane' }, // will be filtered out
{ id: '6', name: 'Bryan' },
{ id: '7', name: 'Bryan' }, // will be filtered out
);
// Custom comparison function that compares the name of the users (case-insensitive)
const customCompare = (a: User, b: User) => a.name.toLowerCase() === b.name.toLowerCase();
// distinctUntilChanged with custom comparison function
users$.pipe(distinctUntilChanged(customCompare)).subscribe(console.log);
// Output:
// { id: '1', name: 'John' }
// { id: '4', name: 'Jane' }
// { id: '6', name: 'Bryan' }
Filtering based on 1 key with distinctUntilKeyChanged
We can simplify the previous example with the distinctUntilKeyChanged
operator. This operator compares emissions based on a specific property (key) instead of the whole object.
type User = { id: string; name: string; };
const users$ = of(
{ id: '1', name: 'John' },
{ id: '2', name: 'John' }, // will be filtered out
{ id: '3', name: 'John' }, // will be filtered out
{ id: '4', name: 'Jane' },
{ id: '5', name: 'Jane' }, // will be filtered out
{ id: '6', name: 'Bryan' },
{ id: '7', name: 'Bryan' }, // will be filtered out
);
// distinctUntilKeyChanged that filters out emissions based on the name property
// please note that the comparison is case-sensitive
users$.pipe(distinctUntilKeyChanged('name')).subscribe(console.log);
// Output:
// { id: '1', name: 'John' }
// { id: '4', name: 'Jane' }
// { id: '6', name: 'Bryan' }
Real-world use case with distinctUntilChanged
in Angular
Filtering out duplicate HTTP requests
In this example, we have a search input that emits every time the user types something in the search bar. We want to filter out duplicate search terms and only send a request to the server if the search term is different from the previous search term.
All of the emissions (keypresses in the search bar) that happens within 1 second are thrown away and only the last value is emitted because of the debounceTime
operator.
We use switchMap
to perform the actual search request.
@Component({
...
template: `<input [formControl]="searchBar" type="text">`
})
export class AppComponent {
...
public readonly searchBar = new FormControl();
public readonly searchResults$ = this.searchBar.valueChanges.pipe(
debounceTime(1000), // Waits for 1000ms or 1s before emitting
distinctUntilChanged(), // Only emits if the new value is different from the previous value
switchMap((query) => this.searchService.search(query)) // Performs the search request
);
constructor() {
this.searchResults$.subscribe((query) => {
console.log(query);
});
}
}
Optimizing View Models
In a previous article about RxJS combineLatest
, we saw how we can create a view model.
We should also optimize the view model by using distinctUntilChanged
to make sure to only emit when one of the properties in the view model has actually changed.
@Component({
...
template: `
<div *ngIf="vm$ | async as vm">
<h1></h1>
<p>Current user: </p>
</div>
`
})
export class AppComponent {
private readonly titleService = inject(TitleService);
private readonly authService = inject(AuthService);
// distinctUntilChanged operator makes sure that the vm$ is
// only emitted when one of the values has actually changed
public readonly vm$ = combineLatest([
this.titleService.getTitle().pipe(distinctUntilChanged()),
this.userService.getCurrentUsername().pipe(distinctUntilChanged())
]).pipe(
map(([title, currentUsername]) => ({ currentUser, currentUsername }))
);
}
Conclusion
The distinctUntilChanged
filters out duplicate emissions, it performs a strict equality check (===) by default but we can provide a custom comparsion function.
When we want to filter on a specific property of a type we can use the distinctUntilKeyChanged
operator.
Other articles you might like
-
Generating icon components from SVG files with NX and Angular
-
Angular + NGINX + Docker
-
How to Call the OpenAI API Directly from Angular (with streaming)
-
Custom TitleStrategy in Angular
-
RxJS catchError: error handling
-
RxJS combineLatest: how it works and how you can use it in Angular
-
Delaying streams with RxJS debounceTime
-
Real-life use cases for RxJS SwitchMap in Angular
-
Transforming data with the RxJS Map operator
-
Typesafe view models with RxJS and Angular
-
Reactively storing and retrieving URL state in Angular
-
Let's build an Image Generator with OpenAI and Angular
-
Why you should externalize your Angular Configuration