|
| 1 | +The `find` operator searches through the sequence of values emitted by a source Observable. It takes a **predicate function** (a function that returns `true` or `false`) as an argument. |
| 2 | + |
| 3 | +`find` will: |
| 4 | + |
| 5 | +1. Check each value emitted by the source against the predicate function. |
| 6 | +2. If it finds a value for which the predicate returns `true`: |
| 7 | + - It emits that **single value**. |
| 8 | + - It immediately **completes** the output Observable (it stops listening to the source). |
| 9 | +3. If the source Observable completes _without_ emitting any value that satisfies the predicate function: |
| 10 | + - `find` emits `undefined`. |
| 11 | + - It then completes. |
| 12 | + |
| 13 | +## Analogy |
| 14 | + |
| 15 | +Imagine you're watching items pass by on a **conveyor belt** (the source Observable). You're looking for a specific item, say, the **first red ball**. |
| 16 | + |
| 17 | +- You start watching (`subscribe`). |
| 18 | +- Items go by: blue square, green triangle... (`find` checks each with your predicate `item => item.color === 'red'`). |
| 19 | +- A red ball appears! (`find` predicate returns `true`). |
| 20 | +- You **grab that red ball** (emit the value). |
| 21 | +- You **walk away** because you found what you needed (complete the output Observable). You don't care about any other items that might come later on the belt. |
| 22 | +- If the belt stops (`source completes`) before you see _any_ red balls, you walk away empty-handed (emit `undefined`). |
| 23 | + |
| 24 | +## Key Points |
| 25 | + |
| 26 | +- **Emits At Most One Value:** You'll only ever get the _first_ matching item or `undefined`. |
| 27 | +- **Completes Early:** As soon as a match is found, the operator completes. This can be efficient if you only need the first occurrence. |
| 28 | +- **Predicate Function:** The core logic lives in the function you provide to test each value. |
| 29 | +- **vs `filter`:** Don't confuse `find` with `filter`. `filter` lets _all_ values that match the predicate pass through, while `find` only lets the _first_ one through and then stops. |
| 30 | + |
| 31 | +## Real-World Example: Finding the First Admin User in a Stream |
| 32 | + |
| 33 | +Suppose you have a stream of user objects being emitted (perhaps from a WebSocket or paginated API results). You want to find the very first user object that has administrative privileges and then stop processing. |
| 34 | + |
| 35 | +### Code Snippet |
| 36 | + |
| 37 | +**1. Mock User Service (Emits Users One by One)** |
| 38 | + |
| 39 | +```typescript |
| 40 | +import { Injectable } from "@angular/core"; |
| 41 | +import { Observable, from, timer } from "rxjs"; |
| 42 | +import { concatMap, delay, tap } from "rxjs/operators"; // Use concatMap for sequential emission with delay |
| 43 | + |
| 44 | +export interface User { |
| 45 | + id: number; |
| 46 | + name: string; |
| 47 | + isAdmin: boolean; |
| 48 | +} |
| 49 | + |
| 50 | +@Injectable({ |
| 51 | + providedIn: "root", |
| 52 | +}) |
| 53 | +export class UserStreamService { |
| 54 | + getUsers(): Observable<User> { |
| 55 | + const users: User[] = [ |
| 56 | + { id: 1, name: "Alice (User)", isAdmin: false }, |
| 57 | + { id: 2, name: "Bob (User)", isAdmin: false }, |
| 58 | + { id: 3, name: "Charlie (Admin)", isAdmin: true }, // The one we want! |
| 59 | + { id: 4, name: "Diana (User)", isAdmin: false }, |
| 60 | + { id: 5, name: "Eve (Admin)", isAdmin: true }, // `find` won't reach this one |
| 61 | + ]; |
| 62 | + |
| 63 | + console.log("UserStreamService: Starting user emission..."); |
| 64 | + |
| 65 | + // Emit users one by one with a small delay between them |
| 66 | + return from(users).pipe( |
| 67 | + concatMap((user) => |
| 68 | + timer(500).pipe( |
| 69 | + // Wait 500ms before emitting next user |
| 70 | + tap(() => console.log(` -> Emitting user: ${user.name}`)), |
| 71 | + switchMap(() => of(user)) // Emit the user after the delay |
| 72 | + ) |
| 73 | + ) |
| 74 | + // This simpler version emits immediately, find still works: |
| 75 | + // return from(users).pipe( |
| 76 | + // tap(user => console.log(` -> Emitting user: ${user.name}`)) |
| 77 | + // ); |
| 78 | + ); |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +**2. Component Using `find`** |
| 84 | + |
| 85 | +```typescript |
| 86 | +import { |
| 87 | + Component, |
| 88 | + inject, |
| 89 | + signal, |
| 90 | + ChangeDetectionStrategy, |
| 91 | + OnInit, |
| 92 | + DestroyRef, |
| 93 | +} from "@angular/core"; |
| 94 | +import { CommonModule } from "@angular/common"; // For @if and json pipe |
| 95 | +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; |
| 96 | +import { UserStreamService, User } from "./user-stream.service"; // Adjust path |
| 97 | +import { find, tap } from "rxjs/operators"; |
| 98 | + |
| 99 | +@Component({ |
| 100 | + selector: "app-find-admin", |
| 101 | + standalone: true, |
| 102 | + imports: [CommonModule], |
| 103 | + template: ` |
| 104 | + <div> |
| 105 | + <h4>Find Operator Example</h4> |
| 106 | + <p>Searching for the first admin user in the stream...</p> |
| 107 | +
|
| 108 | + @if (foundAdmin()) { |
| 109 | + <div class="result found"> |
| 110 | + <strong>First Admin Found:</strong> |
| 111 | + <pre>{{ foundAdmin() | json }}</pre> |
| 112 | + </div> |
| 113 | + } @else if (searchComplete()) { |
| 114 | + <p class="result not-found"> |
| 115 | + No admin user found before the stream completed. |
| 116 | + </p> |
| 117 | + } @else { |
| 118 | + <p class="result searching">Searching...</p> |
| 119 | + } |
| 120 | + </div> |
| 121 | + `, |
| 122 | + // No 'styles' section |
| 123 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 124 | +}) |
| 125 | +export class FindAdminComponent implements OnInit { |
| 126 | + private userStreamService = inject(UserStreamService); |
| 127 | + private destroyRef = inject(DestroyRef); |
| 128 | + |
| 129 | + // --- State Signals --- |
| 130 | + foundAdmin = signal<User | undefined>(undefined); // Result can be User or undefined |
| 131 | + searchComplete = signal<boolean>(false); // Track if the find operation finished |
| 132 | + |
| 133 | + ngOnInit(): void { |
| 134 | + console.log("FindAdminComponent: Subscribing to find the first admin..."); |
| 135 | + |
| 136 | + this.userStreamService |
| 137 | + .getUsers() |
| 138 | + .pipe( |
| 139 | + tap((user) => |
| 140 | + console.log(`Checking user: ${user.name}, isAdmin: ${user.isAdmin}`) |
| 141 | + ), |
| 142 | + |
| 143 | + // --- Apply the find operator --- |
| 144 | + // Predicate checks the isAdmin property |
| 145 | + find((user) => user.isAdmin === true), |
| 146 | + // -------------------------------- |
| 147 | + |
| 148 | + takeUntilDestroyed(this.destroyRef) // Standard cleanup |
| 149 | + ) |
| 150 | + .subscribe({ |
| 151 | + next: (adminUser) => { |
| 152 | + // This 'next' block runs AT MOST ONCE. |
| 153 | + // 'adminUser' will be the first user where isAdmin is true, OR undefined. |
| 154 | + if (adminUser) { |
| 155 | + console.log("SUCCESS: First admin found ->", adminUser); |
| 156 | + this.foundAdmin.set(adminUser); |
| 157 | + } else { |
| 158 | + // This case happens if the source stream completes BEFORE an admin is found. |
| 159 | + console.log( |
| 160 | + "INFO: Stream completed without finding an admin user." |
| 161 | + ); |
| 162 | + } |
| 163 | + this.searchComplete.set(true); // Mark search as finished |
| 164 | + }, |
| 165 | + error: (err) => { |
| 166 | + console.error("Error during user stream processing:", err); |
| 167 | + this.searchComplete.set(true); // Mark as finished on error too |
| 168 | + }, |
| 169 | + complete: () => { |
| 170 | + // This 'complete' runs immediately after 'find' emits its value (or undefined). |
| 171 | + // It does NOT wait for the source stream ('getUsers') to necessarily finish |
| 172 | + // if an admin was found early. |
| 173 | + console.log("Find operation stream completed."); |
| 174 | + // Ensure completion state is set, e.g., if source was empty. |
| 175 | + this.searchComplete.set(true); |
| 176 | + }, |
| 177 | + }); |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +**Explanation:** |
| 183 | + |
| 184 | +1. `UserStreamService` provides an Observable `getUsers()` that emits user objects sequentially with a delay. |
| 185 | +2. `FindAdminComponent` subscribes to this stream in `ngOnInit`. |
| 186 | +3. **`find(user => user.isAdmin === true)`**: This is the core. For each user emitted by `getUsers()`: |
| 187 | + - The predicate `user => user.isAdmin === true` is evaluated. |
| 188 | + - It checks Alice (false), Bob (false). |
| 189 | + - It checks Charlie (true!). The predicate returns `true`. |
| 190 | + - `find` immediately emits the Charlie `User` object. |
| 191 | + - `find` immediately completes its output stream. It unsubscribes from the `getUsers()` source; Diana and Eve will likely not even be processed by the `tap` or emitted by the source in this specific component subscription because `find` stopped listening early. |
| 192 | +4. The `subscribe` block receives the Charlie object in its `next` handler. The `foundAdmin` signal is updated, and the UI displays the result. The `searchComplete` signal is set. |
| 193 | +5. The `complete` handler runs immediately after `next`, logging that the `find` operation is done. |
| 194 | + |
| 195 | +If you were to change the `users` array in the service so no user has `isAdmin: true`, the `getUsers` stream would emit all users and then complete. `find` would never find a match, so it would emit `undefined` when its source completes. The `next` handler would receive `undefined`, the UI would show the "not found" message, and `complete` would run. |
0 commit comments