withLatestFrom
Think of withLatestFrom()
as an operator that lets one stream (the "source") peek at the most recent value from one or more other streams whenever the source stream emits something.
- Source Stream: This is the main Observable you attach
withLatestFrom()
to. - Other Streams: These are the Observables you pass into
withLatestFrom()
. - How it works: When the source stream emits a value,
withLatestFrom()
looks at the other streams and grabs their latest emitted value. It then combines the value from the source stream and the latest values from the other streams into an array. - Important: It only emits when the source stream emits. If the other streams emit values but the source stream hasn't emitted since,
withLatestFrom()
does nothing. Also, it won't emit anything until all the provided streams (source and others) have emitted at least one value.
Real-World Example: Search with Filters#
Imagine you have a search page for products. There's:
- A search input field where the user types their query.
- A dropdown menu to select a category filter (e.g., "Electronics", "Clothing", "Home Goods").
You want to make an API call to fetch products whenever the user types in the search box (after a little pause, using debounceTime
), but you need both the search term and the currently selected category filter to make the correct API request.
- The search term changes frequently. This will be our source stream (after debouncing).
- The category filter changes less often, maybe only when the user explicitly selects a new option. This will be our other stream.
We want to trigger the search using the latest filter value at the moment the (debounced) search term is ready. withLatestFrom()
is perfect for this.
Code Snippet#
Let's see how this looks in an Angular component:
import { Component, inject, DestroyRef, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ReactiveFormsModule, FormControl } from "@angular/forms";
import {
Subject,
debounceTime,
distinctUntilChanged,
withLatestFrom,
takeUntilDestroyed,
startWith,
} from "rxjs";
@Component({
selector: "app-product-search",
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<div>
<label for="search">Search:</label>
<input id="search" type="text" [formControl]="searchTermControl" />
</div>
<div>
<label for="category">Category:</label>
<select id="category" [formControl]="categoryFilterControl">
<option value="all">All</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="home">Home Goods</option>
</select>
</div>
<div *ngIf="searchResults">
Searching for: "{{ searchResults.term }}" in category: "{{
searchResults.category
}}"
</div>
`,
})
export class ProductSearchComponent implements OnInit {
// --- Dependencies ---
private destroyRef = inject(DestroyRef); // For automatic unsubscription
// --- Form Controls ---
searchTermControl = new FormControl("");
categoryFilterControl = new FormControl("all"); // Default category
// --- Component State ---
searchResults: { term: string; category: string } | null = null;
ngOnInit(): void {
// --- Observables ---
// Source: Search term, debounced
const searchTerm$ = this.searchTermControl.valueChanges.pipe(
debounceTime(400), // Wait for 400ms pause in typing
distinctUntilChanged(), // Only emit if the value actually changed
startWith(this.searchTermControl.value || "") // Emit initial value immediately
);
// Other: Category filter
const categoryFilter$ = this.categoryFilterControl.valueChanges.pipe(
startWith(this.categoryFilterControl.value || "all") // Emit initial value immediately
);
// --- Combining with withLatestFrom ---
searchTerm$
.pipe(
withLatestFrom(categoryFilter$), // Combine search term with the LATEST category
takeUntilDestroyed(this.destroyRef) // Auto-unsubscribe when component is destroyed
)
.subscribe(([term, category]) => {
// This block runs ONLY when searchTerm$ emits (after debounce)
// It gets the emitted 'term' and the 'latest' value from categoryFilter$
// Ensure we have non-null values (FormControl can emit null)
const validTerm = term ?? "";
const validCategory = category ?? "all";
console.log(
`API Call Needed: Search for "${validTerm}" with filter "${validCategory}"`
);
// In a real app, you'd call your API service here:
// this.productService.search(validTerm, validCategory).subscribe(...)
// Update component state for display (example)
this.searchResults = { term: validTerm, category: validCategory };
});
}
}
Explanation of the Code:
searchTermControl
/categoryFilterControl
: We use Angular'sFormControl
to manage the input and select elements.searchTerm$
: We get an Observable of the search term's changes usingvalueChanges
. We apply:debounceTime(400)
: To wait until the user stops typing for 400ms before considering the term stable.distinctUntilChanged()
: To avoid triggering searches if the debounced term is the same as the last one.startWith()
: To ensure the stream has an initial value sowithLatestFrom
can emit right away if the category also has a value. This makes the initial state work correctly.
categoryFilter$
: We get an Observable of the category changes usingvalueChanges
. We also usestartWith()
here for the initial value.withLatestFrom(categoryFilter$)
: We pipe thesearchTerm$
(our source). WhensearchTerm$
emits a value (after debouncing),withLatestFrom
looks atcategoryFilter$
and gets its most recently emitted value.subscribe(([term, category]) => ...)
: The result is an array[sourceValue, latestOtherValue]
. We destructure this intoterm
andcategory
. This callback function is executed only when the debounced search term changes. Inside, we have exactly what we need: the current search term and the latest selected category at that moment.takeUntilDestroyed(this.destroyRef)
: This is the modern Angular way to handle unsubscriptions. When theProductSearchComponent
is destroyed, this operator automatically completes the Observable stream, preventing memory leaks without manual cleanup.
So, withLatestFrom()
is incredibly useful when an action (like searching) depends on the latest state of other configuration or filter inputs at the exact moment the action is triggered.