Contents

Angular Enterprise Dashboard - Phase 3B.2: Search-as-You-Type โ€” Abort & Cancellation Patterns

In the previous post, we declared our resource() with a params computation and a loader. The user types a query, and the resource fetches matching projects.

The Art of Cancelling Work You No Longer Need

But what happens when the user types fast? Each keystroke changes the searchQuery signal, which changes params, which triggers a new loader call. Without proper handling, you’d have multiple overlapping requests โ€” a recipe for race conditions, wasted bandwidth, and stale data.

This is where abortSignal saves the day.


Imagine the user types “dat” quickly:

  • Keystroke ’d’ โ†’ Request 1 starts
  • Keystroke ‘a’ โ†’ Request 2 starts (Request 1 is now stale)
  • Keystroke ’t’ โ†’ Request 3 starts (Request 2 is also stale)

Without cancellation, all three requests complete. The last one to resolve “wins” โ€” but network timing is unpredictable. Request 1 (for ’d’) might resolve after Request 3 (for ‘dat’), showing the wrong results.

sequenceDiagram
    participant U as User Types
    participant R as resource()
    participant API as API Service

    U->>R: types 'd'
    R->>API: searchProjects('d')
    U->>R: types 'a' (query='da')
    R->>API: searchProjects('da')
    U->>R: types 't' (query='dat')
    R->>API: searchProjects('dat')
    API-->>R: Response for 'd' (STALE!)
    API-->>R: Response for 'dat' โœ…
    API-->>R: Response for 'da' (STALE, arrives LAST!)
    Note over R: Wrong results shown!
sequenceDiagram
    participant U as User Types
    participant R as resource()
    participant API as API Service

    U->>R: types 'd'
    R->>API: searchProjects('d', signalโ‚)
    U->>R: types 'a' (query='da')
    R-->>API: signalโ‚.abort() โŒ
    R->>API: searchProjects('da', signalโ‚‚)
    U->>R: types 't' (query='dat')
    R-->>API: signalโ‚‚.abort() โŒ
    R->>API: searchProjects('dat', signalโ‚ƒ)
    API-->>R: Response for 'dat' โœ…
    Note over R: Only the correct result arrives!

When params changes, Angular’s resource() automatically creates a new AbortSignal and fires the old one before invoking the new loader. You don’t need to manage this yourself โ€” just pass the signal through.

readonly projectsResource = resource({
  params: () => {
    const query = this.searchQuery();
    return query.length > 0 ? { query } : undefined;
  },

  loader: async ({ params, abortSignal }) => {
    // abortSignal is auto-cancelled when params change
    return this.projectApi.searchProjects(params.query, abortSignal);
  },
});

The API service must respect the signal. Here’s our delay() helper that properly cleans up:

private delay(ms: number, abortSignal?: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, ms);
    abortSignal?.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(new DOMException('Request aborted', 'AbortError'));
    });
  });
}

Key detail: The DOMException with name 'AbortError' is the standard signal for fetch cancellation. Angular’s resource() recognizes this and doesn’t treat it as a failure โ€” it simply discards the result.

In a real production app using fetch(), you’d just pass the signal directly:

// Production version
const response = await fetch(url, { signal: abortSignal });

Notice this line in our params computation:

return query.length > 0 ? { query } : undefined;

When params returns undefined, the resource enters the idle state. The loader is not called. This is intentional โ€” an empty search box means “the user hasn’t asked for anything yet.”

stateDiagram-v2
    [*] --> idle: Initial state
    idle --> loading: params returns value
    loading --> resolved: loader completes
    loading --> error: loader throws
    resolved --> loading: params change
    idle --> idle: params returns undefined

In our template, we handle this with an inviting empty state:

@case ('idle') {
<div class="empty-state">
  <span class="empty-icon">๐Ÿ”</span>
  <p>Type a search query to find projects.</p>
</div>
}

The entire search flow is triggered by a single signal update:

onSearchInput(event: Event): void {
  const value = (event.target as HTMLInputElement).value;
  this.searchQuery.set(value); // This one line triggers everything
}
  1. searchQuery updates โ†’ params re-evaluates โ†’ old request aborted โ†’ new loader runs โ†’ status and value signals update โ†’ template re-renders.

No debounceTime. No switchMap. No takeUntilDestroyed. Just Signals.


We’ve handled the data lifecycle and cancellation. But how does the UI look during each phase? In Phase 3B.3, we’ll build a comprehensive status-driven UI that handles all six ResourceStatus states.


Try typing fast in the search box and watch the browser DevTools network tab. You’ll see cancelled requests โ€” that’s abortSignal at work!

Related Content