Skip to content

Commit 2785629

Browse files
added new operators
1 parent 75d5220 commit 2785629

File tree

16 files changed

+2074
-38
lines changed

16 files changed

+2074
-38
lines changed

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,3 @@ And more as we grow!
3838
Have a great example, question, or operator to add? PRs are open! Let’s make this the go-to resource for RxJS interview prep in the Angular world.
3939

4040
---
41-
42-
Let me know if you want badges, table of contents, or starter files (`CONTRIBUTING.md`, etc.) too!
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
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.
2+
3+
- **Source Stream:** This is the main Observable you attach `withLatestFrom()` to.
4+
- **Other Streams:** These are the Observables you pass _into_ `withLatestFrom()`.
5+
- **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.
6+
- **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.
7+
8+
## Real-World Example: Search with Filters
9+
10+
Imagine you have a search page for products. There's:
11+
12+
1. A search input field where the user types their query.
13+
2. A dropdown menu to select a category filter (e.g., "Electronics", "Clothing", "Home Goods").
14+
15+
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.
16+
17+
- The search term changes frequently. This will be our **source** stream (after debouncing).
18+
- The category filter changes less often, maybe only when the user explicitly selects a new option. This will be our **other** stream.
19+
20+
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.
21+
22+
## Code Snippet
23+
24+
Let's see how this looks in an Angular component:
25+
26+
```typescript
27+
import { Component, inject, DestroyRef, OnInit } from "@angular/core";
28+
import { CommonModule } from "@angular/common";
29+
import { ReactiveFormsModule, FormControl } from "@angular/forms";
30+
import {
31+
Subject,
32+
debounceTime,
33+
distinctUntilChanged,
34+
withLatestFrom,
35+
takeUntilDestroyed,
36+
startWith,
37+
} from "rxjs";
38+
39+
@Component({
40+
selector: "app-product-search",
41+
standalone: true,
42+
imports: [CommonModule, ReactiveFormsModule],
43+
template: `
44+
<div>
45+
<label for="search">Search:</label>
46+
<input id="search" type="text" [formControl]="searchTermControl" />
47+
</div>
48+
<div>
49+
<label for="category">Category:</label>
50+
<select id="category" [formControl]="categoryFilterControl">
51+
<option value="all">All</option>
52+
<option value="electronics">Electronics</option>
53+
<option value="clothing">Clothing</option>
54+
<option value="home">Home Goods</option>
55+
</select>
56+
</div>
57+
58+
<div *ngIf="searchResults">
59+
Searching for: "{{ searchResults.term }}" in category: "{{
60+
searchResults.category
61+
}}"
62+
</div>
63+
`,
64+
})
65+
export class ProductSearchComponent implements OnInit {
66+
// --- Dependencies ---
67+
private destroyRef = inject(DestroyRef); // For automatic unsubscription
68+
69+
// --- Form Controls ---
70+
searchTermControl = new FormControl("");
71+
categoryFilterControl = new FormControl("all"); // Default category
72+
73+
// --- Component State ---
74+
searchResults: { term: string; category: string } | null = null;
75+
76+
ngOnInit(): void {
77+
// --- Observables ---
78+
// Source: Search term, debounced
79+
const searchTerm$ = this.searchTermControl.valueChanges.pipe(
80+
debounceTime(400), // Wait for 400ms pause in typing
81+
distinctUntilChanged(), // Only emit if the value actually changed
82+
startWith(this.searchTermControl.value || "") // Emit initial value immediately
83+
);
84+
85+
// Other: Category filter
86+
const categoryFilter$ = this.categoryFilterControl.valueChanges.pipe(
87+
startWith(this.categoryFilterControl.value || "all") // Emit initial value immediately
88+
);
89+
90+
// --- Combining with withLatestFrom ---
91+
searchTerm$
92+
.pipe(
93+
withLatestFrom(categoryFilter$), // Combine search term with the LATEST category
94+
takeUntilDestroyed(this.destroyRef) // Auto-unsubscribe when component is destroyed
95+
)
96+
.subscribe(([term, category]) => {
97+
// This block runs ONLY when searchTerm$ emits (after debounce)
98+
// It gets the emitted 'term' and the 'latest' value from categoryFilter$
99+
100+
// Ensure we have non-null values (FormControl can emit null)
101+
const validTerm = term ?? "";
102+
const validCategory = category ?? "all";
103+
104+
console.log(
105+
`API Call Needed: Search for "${validTerm}" with filter "${validCategory}"`
106+
);
107+
108+
// In a real app, you'd call your API service here:
109+
// this.productService.search(validTerm, validCategory).subscribe(...)
110+
111+
// Update component state for display (example)
112+
this.searchResults = { term: validTerm, category: validCategory };
113+
});
114+
}
115+
}
116+
```
117+
118+
**Explanation of the Code:**
119+
120+
1. **`searchTermControl` / `categoryFilterControl`:** We use Angular's `FormControl` to manage the input and select elements.
121+
2. **`searchTerm$`:** We get an Observable of the search term's changes using `valueChanges`. We apply:
122+
- `debounceTime(400)`: To wait until the user stops typing for 400ms before considering the term stable.
123+
- `distinctUntilChanged()`: To avoid triggering searches if the debounced term is the same as the last one.
124+
- `startWith()`: To ensure the stream has an initial value so `withLatestFrom` can emit right away if the category also has a value. This makes the initial state work correctly.
125+
3. **`categoryFilter$`:** We get an Observable of the category changes using `valueChanges`. We also use `startWith()` here for the initial value.
126+
4. **`withLatestFrom(categoryFilter$)`:** We pipe the `searchTerm$` (our source). When `searchTerm$` emits a value (after debouncing), `withLatestFrom` looks at `categoryFilter$` and gets its _most recently emitted value_.
127+
5. **`subscribe(([term, category]) => ...)`:** The result is an array `[sourceValue, latestOtherValue]`. We destructure this into `term` and `category`. 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.
128+
6. **`takeUntilDestroyed(this.destroyRef)`:** This is the modern Angular way to handle unsubscriptions. When the `ProductSearchComponent` is destroyed, this operator automatically completes the Observable stream, preventing memory leaks without manual cleanup.
129+
130+
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.

docs/Operators/Combination/zip.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
The `zip()` operator in RxJS is a **combination operator**. Its job is to combine multiple source Observables by waiting for each observable to emit a value at the **same index**, and then it emits an array containing those values paired together.
2+
3+
## Analogy: The Zipper
4+
5+
Think of a clothing zipper. It has two sides (or more, if you imagine a multi-way zipper!). To close the zipper, teeth from _both_ sides must align at the same position. `zip()` works the same way:
6+
7+
1. It subscribes to all the source Observables you provide.
8+
2. It waits until the **first** value arrives from **every** source. It then emits these first values together in an array: `[firstValueA, firstValueB, ...]`.
9+
3. Then, it waits until the **second** value arrives from **every** source. It emits these second values together: `[secondValueA, secondValueB, ...]`.
10+
4. It continues this process, index by index (0, 1, 2,...).
11+
5. **Crucially:** If one source Observable completes _before_ another, `zip()` will stop emitting new pairs as soon as it runs out of values from the shorter source to pair with. It needs a value from _all_ sources for a given index to emit.
12+
13+
## Why Use `zip()`?
14+
15+
You use `zip()` when you have multiple streams and you need to combine their values based on their **emission order or index**. You specifically want the 1st item from stream A paired with the 1st from stream B, the 2nd with the 2nd, and so on.
16+
17+
## Real-World Example: Pairing Related Sequential Data
18+
19+
Imagine you have two real-time data feeds:
20+
21+
1. `sensorA$` emits temperature readings every second.
22+
2. `sensorB$` emits humidity readings every second, perfectly synchronized with sensor A.
23+
24+
You want to process these readings as pairs (temperature and humidity for the _same_ timestamp/interval). `zip` is perfect for this.
25+
26+
Another scenario (less common for APIs, more for UI events or other streams): Suppose you want to pair every user click with a corresponding item from another list that gets populated sequentially. The first click pairs with the first item, the second click with the second, etc.
27+
28+
## Code Snippet Example
29+
30+
Let's create a simple Angular component example using `zip`. We'll zip together values from two simple streams: one emitting letters ('A', 'B', 'C') quickly, and another emitting numbers (10, 20, 30, 40) more slowly.
31+
32+
```typescript
33+
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
34+
import { zip, interval, of } from "rxjs";
35+
import { map, take } from "rxjs/operators";
36+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
37+
38+
@Component({
39+
selector: "app-zip-example",
40+
standalone: true,
41+
template: `
42+
<h2>RxJS zip() Example</h2>
43+
<p>Combining letters and numbers based on index:</p>
44+
<ul>
45+
<li *ngFor="let pair of zippedResult()">{{ pair | json }}</li>
46+
</ul>
47+
<p>
48+
Note: The number stream had '40', but the letter stream completed after
49+
'C', so zip stopped.
50+
</p>
51+
`,
52+
styles: [
53+
`
54+
li {
55+
font-family: monospace;
56+
}
57+
`,
58+
],
59+
})
60+
export class ZipExampleComponent implements OnInit {
61+
private destroyRef = inject(DestroyRef);
62+
63+
zippedResult = signal<Array<[string, number]>>([]); // Signal to hold the result
64+
65+
ngOnInit() {
66+
// Source 1: Emits 'A', 'B', 'C' one after another immediately
67+
const letters$ = of("A", "B", "C");
68+
69+
// Source 2: Emits 10, 20, 30, 40 every 500ms
70+
const numbers$ = interval(500).pipe(
71+
map((i) => (i + 1) * 10), // Map index 0, 1, 2, 3 to 10, 20, 30, 40
72+
take(4) // Only take the first 4 values
73+
);
74+
75+
// Zip them together
76+
zip(letters$, numbers$)
77+
.pipe(
78+
// zip emits arrays like [string, number]
79+
takeUntilDestroyed(this.destroyRef) // Auto-unsubscribe
80+
)
81+
.subscribe({
82+
next: (value) => {
83+
// Update the signal with the latest pair
84+
// NOTE: For signals, it's often better to collect all results
85+
// if the stream completes quickly, or update progressively.
86+
// Here we'll just append for demonstration.
87+
this.zippedResult.update((current) => [...current, value]);
88+
console.log("Zipped value:", value);
89+
},
90+
complete: () => {
91+
console.log("Zip completed because the letters$ stream finished.");
92+
},
93+
error: (err) => {
94+
console.error("Zip error:", err);
95+
},
96+
});
97+
}
98+
}
99+
```
100+
101+
**Explanation of the Code:**
102+
103+
1. `letters$` emits 'A', 'B', 'C' and then completes.
104+
2. `numbers$` starts emitting 10 (at 500ms), 20 (at 1000ms), 30 (at 1500ms), 40 (at 2000ms).
105+
3. `zip` waits:
106+
- It gets 'A' immediately, but waits for `numbers$` to emit.
107+
- At 500ms, `numbers$` emits 10. `zip` now has the first value from both ('A', 10) -> Emits `['A', 10]`.
108+
- It gets 'B' immediately, waits for `numbers$`.
109+
- At 1000ms, `numbers$` emits 20. `zip` has the second value from both ('B', 20) -> Emits `['B', 20]`.
110+
- It gets 'C' immediately, waits for `numbers$`.
111+
- At 1500ms, `numbers$` emits 30. `zip` has the third value from both ('C', 30) -> Emits `['C', 30]`.
112+
4. `letters$` has now completed. Even though `numbers$` emits 40 at 2000ms, `zip` cannot find a corresponding 4th value from `letters$`, so it stops and completes.
113+
114+
## `zip()` vs. Other Combination Operators
115+
116+
- **`combineLatest`**: Emits an array of the _latest_ values from each source whenever _any_ source emits. Doesn't care about index, just the most recent value from all participants.
117+
- **`forkJoin`**: Waits for _all_ source observables to _complete_, then emits a single array containing the _last_ value emitted by each source. Useful for running parallel one-off tasks (like multiple HTTP requests) and getting all results together at the end.
118+
119+
Use `zip()` specifically when the _order_ and _pairing_ by index (1st with 1st, 2nd with 2nd, etc.) is what you need.

0 commit comments

Comments
 (0)