Simplifying Async Operations in Angular with the resource API

Handling asynchronous data is a common challenge in Angular applications. Traditionally, we’ve relied on Observables and Promises, but Angular’s new resource API provides a more integrated and reactive way to handle async data using signals.

Why Was the resource API Introduced?

Observables are powerful but can sometimes lead to complex and hard-to-follow code, especially when dealing with multiple streams of data. The resource API was introduced to make managing async operations easier and more declarative. It integrates smoothly with Angular’s signals, making it easier to react to data changes.

As Jeremy Elbourn and Mark Thompson mentioned in a recent live Q/A, they have been getting mixed feedback for RxJS and Observables in general. Some people loved it and others absolutely hated it which lead to the introduction of this new API called resource which will statisfy working with Promises while leveraging signals and Angular's reactivity features.

It's worth mentioning that there is also a similar API called rxResource which is the alternative of resource but for using it with Observables instead of Promises. At the time of this post there currently isn't any documentation for rxResource but you can find it in the reference docs https://angular.dev/api/core/rxjs-interop/rxResource

Benefits of the resource API:

  • Declarative: Removes the need for manual subscription management.
  • Reactive: Works seamlessly with signals, automatically updating when dependencies change.
  • Efficient: Cancels in-progress requests when inputs change, reducing unnecessary network calls.

How the resource API Works

The resource function creates a reactive request to an async function using a loader function. It automatically tracks dependencies, so when inputs change, the request is re-executed.

Here’s a basic example of using resource to fetch user data:

import { resource, Signal } from "@angular/core";

// Simulate a signal holding the user ID
const userId: Signal<number> = signal(1);

// Resource to fetch user details
const userResource = resource({
  request: () => ({ id: userId() }), // Reactive input (auto-updates)
  loader: ({ request }) => fetchUser(request.id), // Async fetch function
});

// Function to fetch user data
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );
  return response.json();
}

// Using the resource in a component
console.log(userResource.value()); // Prints latest user data
console.log(userResource.isLoading()); // True if loading
console.log(userResource.error()); // Prints error if fetch fails

Breakdown:

  • request: Defines what data triggers a new fetch (here, the userId signal).
  • loader: Defines the async function that fetches data when request changes.
  • value(): Holds the latest loaded data.
  • isLoading(): Tracks whether the fetch operation is in progress.
  • error(): Stores any errors encountered.

In the example above, whenever the userId signal changes, it will also trigger the loader callback and thus creating a new http request with the updated request parameters.

Fetching API Data with Dependencies

Another example where we dynamically load posts based on the selected user:

import { resource, Signal } from "@angular/core";

const userId = signal(1);

// Resource to fetch user posts when userId changes
const postsResource = resource({
  request: () => ({ userId: userId() }),
  loader: async ({ request }) => {
    const res = await fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${request.userId}`
    );
    return res.json();
  },
});

// Function to update the user ID dynamically
function changeUser(id: number) {
  userId.set(id);
}

// Using the resource
console.log(postsResource.value()); // Latest posts for user
console.log(postsResource.isLoading()); // True if loading

What Happens Here?

  1. The userId signal holds the selected user ID.
  2. The postsResource fetches posts for that user whenever userId changes.
  3. The data automatically updates in the UI.

Handling Errors in resource

Errors can occur while fetching data. The resource API provides an error() signal to track failed requests.

Here’s how to handle errors using the error() signal

const userResource = resource({
  request: () => ({ id: userId() }),
  loader: async ({ request }) => {
    const response = await fetch(`https://api.example.com/user/${request.id}`);
    if (!response.ok) throw new Error("Failed to fetch user data");
    return response.json();
  },
});

// Do something with the error if it's present
if (userResource.error()) {
  console.error("Error:", userResource.error());
}

More Use Cases using resource API

1. Fetching User Profile Data

Ideal for loading user details when a user logs in or switches profiles.

const profileResource = resource({
  request: () => ({ id: currentUserId() }),
  loader: ({ request }) => fetchUserProfile(request.id),
});

2. Dynamic Search with Debouncing

Useful for fetching search results dynamically when the user types in a search bar.

import { debounceTime } from "rxjs";

const searchQuery = signal("");
const searchResults = resource({
  request: () => ({ query: searchQuery() }),
  loader: async ({ request }) => {
    await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate debounce
    return fetchResults(request.query);
  },
});

3. Loading Configurations from an API

Fetching app-wide configuration settings that update in real-time.

const configResource = resource({
  request: () => ({ env: environment() }),
  loader: ({ request }) => fetchConfig(request.env),
});

Summary

The resource API provides a declarative and reactive way to handle async operations in Angular. By integrating with signals, it simplifies how we fetch and manage async data.

Key Takeaways:

✅ Works with signals for automatic reactivity.
✅ Eliminates manual subscriptions for async data.
✅ Provides built-in loading and error states.
Efficient, canceling previous requests when inputs change.

More posts in JavaScript