Angular: From Zero to Expert

Angular: From Zero to Expert

A Technical Deep Dive into Angular's resource(), rxResource() and httpResource() APIs

Angular 19 introduces a suite of Resource APIs that help manage asynchronous dependencies through the signal system. This evolution positions Angular to deliver better Core Web Vitals.

Amos Isaila's avatar
Amos Isaila
Nov 10, 2024
∙ Paid

Resource API

To create a resource, a developer uses the resource() function, which accepts a ResourceOptions object with two main properties: params and loader or stream.

The params property is a reactive computation that generates a parameter value for the asynchronous operation. When any signals read within this computation change, a new parameter value is produced, and the resource automatically re-fetches the data by invoking the loader function.

The loader is an asynchronous function that performs the actual data retrieval, such as a fetch() call or a Promise-based operation.

Angular: From Zero to Expert is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.

Loader vs Stream

Angular 20 introduces a crucial distinction in resource() between loader and stream that goes beyond just Promise vs Observable handling—it's about single-value vs multi-value data flows:

When to use loader (Single-value operations):

// Use loader for one-time data fetching that returns a single result
const userResource = resource({
  params: () => this.userId,
  loader: async (loaderParams: ResourceLoaderParams<string>) => {
    const response = await fetch(`/api/users/${loaderParams.params}`, { 
      signal: loaderParams.abortSignal 
    });
    return response.json() as User; // Single value returned
  }
});

When to use stream (Multi-value streaming operations):

// Use stream for data that emits multiple values over time
const liveNotificationsResource = resource({
  params: () => ({ userId: this.currentUserId }),
  stream: async (loaderParams) => {
    const userId = loaderParams.params.userId;
    
    // 1. Create Signal representing the stream
    const resultSignal = signal<ResourceStreamItem<Notification[]>>({
      value: []
    });
    
    // 2. Set up WebSocket connection for live notifications
    const ws = new WebSocket(`wss://api.example.com/notifications/${userId}`);
    
    ws.onmessage = (event) => {
      const newNotification = JSON.parse(event.data) as Notification;
      const currentNotifications = resultSignal().value || [];
      resultSignal.set({ 
        value: [newNotification, ...currentNotifications] 
      });
    };
    
    ws.onerror = () => {
      resultSignal.set({ 
        error: new Error('Connection failed') 
      });
    };
    
    // 3. Set up cleanup when stream is no longer needed
    loaderParams.abortSignal.addEventListener('abort', () => {
      ws.close();
    });
    
    // 4. Return the streaming signal
    return resultSignal;
  }
});

Key Architectural Differences:

  • loader: Returns Promise<T> - designed for traditional async operations that resolve once

  • stream: Returns Signal<ResourceStreamItem<T>> | PromiseLike<Signal<ResourceStreamItem<T>>> | undefined - designed for continuous data emission

  • Use cases for loader: HTTP requests, file operations, single database queries

  • Use cases for stream: Real-time data, timers, WebSocket connections, live updates

  • Error handling: Streams can recover from errors and continue emitting values, while loader errors terminate the operation

Update (Angular 22.0.0-next.8): The stream loader signature has been expanded. It now accepts three return types:

type ResourceStreamingLoader<T, R> = (
  param: ResourceLoaderParams<R>
) => Signal<ResourceStreamItem<T>>                    // synchronous
   | PromiseLike<Signal<ResourceStreamItem<T>>>       // async (original)
   | undefined;                                       // skip loading

This is a big deal for SSR. Previously, stream resources always started in Loading state because the return was always async. Now you can return a Signal synchronously — which means cached data from TransferState can be set immediately without a loading flicker. rxResource with synchronous observables (like of(value)) also resolves immediately after a tick instead of requiring await appRef.whenStable().

Stream Semantics: Streaming resources use switch-map semantics—when params change, the old stream is cancelled and a new one begins. This prevents memory leaks and ensures only the most recent stream is active.

This architectural choice allows resource() to handle both traditional async patterns and modern streaming requirements within a unified API, while maintaining the reactive benefits of Angular's signal system.

Resource States

Resources can be in one of these states (ResourceStatus enum):

  • Idle: no valid request, no loading.

  • Loading: initial load for a request.

  • Reloading: loading fresh data for the same request.

  • Resolved: successfully loaded.

  • Error: loading failed.

  • Local: value was set manually.

Original vs Resource-based Implementation

I have a cat’s facts app built with Signals and we are going to refactor the request to be able to use the resource() API.

First, let's look at how we can refactor the CatsFactsService:

User's avatar

Continue reading this post for free, courtesy of Amos Isaila.

Or purchase a paid subscription.
© 2026 Amos Isaila · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture