Skip to content

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:

  1. Check each value emitted by the source against the predicate function.
  2. 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).
  3. If the source Observable completes without emitting any value that satisfies the predicate function:
    • find emits undefined.
    • 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 predicate item => item.color === 'red').
  • A red ball appears! (find predicate returns true).
  • 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 (emit undefined).

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 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.

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:

  1. UserStreamService provides an Observable getUsers() that emits user objects sequentially with a delay.
  2. FindAdminComponent subscribes to this stream in ngOnInit.
  3. find(user => user.isAdmin === true): This is the core. For each user emitted by getUsers():
    • 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 Charlie User object.
    • 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.
  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.
  5. The complete handler runs immediately after next, logging that the find 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.