Streamlining Data Fetching in Angular 19 with resource and rxResource

Authors
Posted on
Posted on

Streamlining Data Fetching in Angular 19 with resource and rxResource

Angular 19 introduces some exciting new features for handling asynchronous operations and simplifying data fetching. In this article, we'll explore the experimental resource and rxResource methods, which offer powerful alternatives to traditional approaches like computed, effect for triggering API calls. And handling data, progress of API calls, and errors in separate variables.

The Problem with Traditional Approaches

Previously, managing asynchronous data fetching and updates in Angular often involved complex combinations of signals, effects, and manual state management. Let's consider a scenario where we fetch user posts and their comments from an API. Using signals and effects, our component code might look something like this:

selectedUser = signal<User | null>(null)
selectedPost = signal<Post | null>(null)
posts = signal<Post[]>([])
loadingPosts = signal<boolean>(false)
postsError = signal<Error | null>()
comments = signal<Comment[]>([])
loadingComments = signal<boolean>(false)
commentsError = signal<Error | null>()

getUserPosts = effect(async () => {
  const user = this.selectedUser()
  this.postsError.set(null)
  this.loadingPosts.set(true)
  try {
    const posts = await firstValueFrom(this.postService.getPosts(user.id))
    this.posts.set(posts)
  } catch (err) {
    this.postsError(err)
  } finally {
    this.loadingPosts.set(false)
  }
})

getPostComments = effect(async () => {
  const post = this.selectedPost()
  if (!post) {
    return
  }
  this.commentsError.set(null)
  this.loadingComments.set(true)
  try {
    const comments = await firstValueFrom(this.postService.getComments(post.id))
    this.comments.set(comments)
  } catch (err) {
    this.commentsError.set(err)
  } finally {
    this.loadingComments.set(false)
  }
})

As you can see, this approach requires managing multiple signals for loading states, error states, and selected data, along with asynchronous effects for fetching posts and comments. There's a potential for errors, such as forgetting to reset the selected post or handling error cases gracefully.

Enter resource

Angular 19's resource method streamlines this process significantly. It simplifies fetching data with automatic handling of loading and error states. Let's refactor the getUserPosts functionality using the resource method from @angular/core:

userPosts = resource<Post[], User>({
  request: () => this.selectedUser(),
  loader: ({ request: user }) => {
    return fetch(`https://jsonplaceholder.typicode.com/users/${user.id}/posts`).then((res) =>
      res.json()
    )
  },
})

The resource function returns an object of type ResourceRef<T> which extends a Resource<T> eventually. In simpler terms, a Resource is an asynchronous depdendency. For example, an API call. However, instead of promises or observables, a a resource is delivered through signals.

A Resource<T> has the following signals available:

/**
 * The current value of the `Resource`, or `undefined` if there is no current value.
 */
readonly value: Signal<T | undefined>;

/**
 * The current status of the `Resource`, which describes what the resource is currently doing and
 * what can be expected of its `value`.
 */
readonly status: Signal<ResourceStatus>;

/**
 * When in the `error` state, this returns the last known error from the `Resource`.
 */
readonly error: Signal<unknown>;

/**
 * Whether this resource is loading a new value (or reloading the existing one).
 */
readonly isLoading: Signal<boolean>;

At the time of writing this article, resource and rxResource methods are still in the experimental stage

With resource, we define a request function that requires a source signal (selectedUser), and a loader function that performs the actual data fetching using fetch. resource automatically creates derived signals for isLoading, error, value, and others, which we can use directly in our template:

@if(userPosts.isLoading()) {
<div>Loading...</div>
} @else if (userPosts.error()) {
<div>Error: {{ userPosts.error() }}</div>
} @else {
<app-todo-list [posts]="userPosts.value()"></app-todo-list>
}

This eliminates the need for manual management of loading and error states, making the code much cleaner and more readable.

Working with Observables: rxResource

If you prefer working with RxJS Observables, Angular 19 provides the rxResource method from the @angular/core/rxjs-interop package. It's similar to resource, but the loader function returns an Observable. Here's how you can rewrite the userPosts resource using rxResource and your existing PostService which already uses observables:

userPosts = rxResource<Post[], User>({
  request: () => this.selectedUser(),
  loader: ({ request: user }) => this.postService.getPosts(user.id),
})

Notice how loader now directly returns the Observable returned by this.postService.getPosts. The same derived signals (isLoading, error, value) are available with rxResource as well.

Error Handling and Conditional Fetching

Both resource and rxResource provide a simple mechanism to handle errors. The error signal contains the error object if the fetch operation fails. You can conditionally display an error message in your template using it as shown in the template example above.

You can also handle cases where you might not always need to make a request. For instance, if there's no selected post, we shouldn't attempt to fetch its comments. You can add a conditional check within the loader function:

postComments = rxResource<Comment[], Post | null>({
  request: () => this.selectedPost(),
  loader: ({ request: post }) => {
    if (!post) {
      return of([]) // Return an empty array if no post is selected
    }
    return this.postService.getComments(post.id)
  },
})

Conclusion

Angular 19's resource and rxResource methods significantly simplify asynchronous operations and data fetching by abstracting away boilerplate code for loading and error handling. While still experimental, these functions demonstrate the direction Angular is heading towards cleaner and more declarative code. As they become stable, they promise to become a cornerstone for managing data interactions in Angular applications.