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, theuserId
signal).loader
: Defines the async function that fetches data whenrequest
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?
- The
userId
signal holds the selected user ID. - The
postsResource
fetches posts for that user wheneveruserId
changes. - 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.