find
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.
find
will:
- Check each value emitted by the source against the predicate function.
- If it finds a value for which the predicate returns
true
:- It emits that single value.
- It immediately completes the output Observable (it stops listening to the source).
- If the source Observable completes without emitting any value that satisfies the predicate function:
find
emitsundefined
.- It then completes.
Analogy#
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.
- You start watching (
subscribe
). - Items go by: blue square, green triangle... (
find
checks each with your predicateitem => item.color === 'red'
). - A red ball appears! (
find
predicate returnstrue
). - You grab that red ball (emit the value).
- 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.
- If the belt stops (
source completes
) before you see any red balls, you walk away empty-handed (emitundefined
).
Key Points#
- Emits At Most One Value: You'll only ever get the first matching item or
undefined
. - Completes Early: As soon as a match is found, the operator completes. This can be efficient if you only need the first occurrence.
- Predicate Function: The core logic lives in the function you provide to test each value.
- vs
filter
: Don't confusefind
withfilter
.filter
lets all values that match the predicate pass through, whilefind
only lets the first one through and then stops.
Real-World Example: Finding the First Admin User in a Stream#
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.
Code Snippet#
1. Mock User Service (Emits Users One by One)
import { Injectable } from "@angular/core";
import { Observable, from, timer } from "rxjs";
import { concatMap, delay, tap } from "rxjs/operators"; // Use concatMap for sequential emission with delay
export interface User {
id: number;
name: string;
isAdmin: boolean;
}
@Injectable({
providedIn: "root",
})
export class UserStreamService {
getUsers(): Observable<User> {
const users: User[] = [
{ id: 1, name: "Alice (User)", isAdmin: false },
{ id: 2, name: "Bob (User)", isAdmin: false },
{ id: 3, name: "Charlie (Admin)", isAdmin: true }, // The one we want!
{ id: 4, name: "Diana (User)", isAdmin: false },
{ id: 5, name: "Eve (Admin)", isAdmin: true }, // `find` won't reach this one
];
console.log("UserStreamService: Starting user emission...");
// Emit users one by one with a small delay between them
return from(users).pipe(
concatMap((user) =>
timer(500).pipe(
// Wait 500ms before emitting next user
tap(() => console.log(` -> Emitting user: ${user.name}`)),
switchMap(() => of(user)) // Emit the user after the delay
)
)
// This simpler version emits immediately, find still works:
// return from(users).pipe(
// tap(user => console.log(` -> Emitting user: ${user.name}`))
// );
);
}
}
2. Component Using find
import {
Component,
inject,
signal,
ChangeDetectionStrategy,
OnInit,
DestroyRef,
} from "@angular/core";
import { CommonModule } from "@angular/common"; // For @if and json pipe
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { UserStreamService, User } from "./user-stream.service"; // Adjust path
import { find, tap } from "rxjs/operators";
@Component({
selector: "app-find-admin",
standalone: true,
imports: [CommonModule],
template: `
<div>
<h4>Find Operator Example</h4>
<p>Searching for the first admin user in the stream...</p>
@if (foundAdmin()) {
<div class="result found">
<strong>First Admin Found:</strong>
<pre>{{ foundAdmin() | json }}</pre>
</div>
} @else if (searchComplete()) {
<p class="result not-found">
No admin user found before the stream completed.
</p>
} @else {
<p class="result searching">Searching...</p>
}
</div>
`,
// No 'styles' section
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FindAdminComponent implements OnInit {
private userStreamService = inject(UserStreamService);
private destroyRef = inject(DestroyRef);
// --- State Signals ---
foundAdmin = signal<User | undefined>(undefined); // Result can be User or undefined
searchComplete = signal<boolean>(false); // Track if the find operation finished
ngOnInit(): void {
console.log("FindAdminComponent: Subscribing to find the first admin...");
this.userStreamService
.getUsers()
.pipe(
tap((user) =>
console.log(`Checking user: ${user.name}, isAdmin: ${user.isAdmin}`)
),
// --- Apply the find operator ---
// Predicate checks the isAdmin property
find((user) => user.isAdmin === true),
// --------------------------------
takeUntilDestroyed(this.destroyRef) // Standard cleanup
)
.subscribe({
next: (adminUser) => {
// This 'next' block runs AT MOST ONCE.
// 'adminUser' will be the first user where isAdmin is true, OR undefined.
if (adminUser) {
console.log("SUCCESS: First admin found ->", adminUser);
this.foundAdmin.set(adminUser);
} else {
// This case happens if the source stream completes BEFORE an admin is found.
console.log(
"INFO: Stream completed without finding an admin user."
);
}
this.searchComplete.set(true); // Mark search as finished
},
error: (err) => {
console.error("Error during user stream processing:", err);
this.searchComplete.set(true); // Mark as finished on error too
},
complete: () => {
// This 'complete' runs immediately after 'find' emits its value (or undefined).
// It does NOT wait for the source stream ('getUsers') to necessarily finish
// if an admin was found early.
console.log("Find operation stream completed.");
// Ensure completion state is set, e.g., if source was empty.
this.searchComplete.set(true);
},
});
}
}
Explanation:
UserStreamService
provides an ObservablegetUsers()
that emits user objects sequentially with a delay.FindAdminComponent
subscribes to this stream inngOnInit
.find(user => user.isAdmin === true)
: This is the core. For each user emitted bygetUsers()
:- The predicate
user => user.isAdmin === true
is evaluated. - It checks Alice (false), Bob (false).
- It checks Charlie (true!). The predicate returns
true
. find
immediately emits the CharlieUser
object.find
immediately completes its output stream. It unsubscribes from thegetUsers()
source; Diana and Eve will likely not even be processed by thetap
or emitted by the source in this specific component subscription becausefind
stopped listening early.
- The predicate
- The
subscribe
block receives the Charlie object in itsnext
handler. ThefoundAdmin
signal is updated, and the UI displays the result. ThesearchComplete
signal is set. - The
complete
handler runs immediately afternext
, logging that thefind
operation is done.
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.