Everything you need to know about the resource API

In v19, Angular will introduce a new API for loading resources. This would allow us to fetch data from an api, know about the status of the request, and update the data locally when needed.

Intro

The resource API is very simple in it's core. Let's see the most simple example of how to use it.

import { resource } from "@angular/core";
 
@Component({})
export class MyComponent {
  todoResource = resource({
    loader: () => {
      return Promise.resolve({ id: 1, title: "Hello World", completed: false });
    },
  });
 
  constructor() {
    effect(() => {
        console.log("Value: ", this.todoResource.value());
        console.log("Status: ", this.todoResource.status());
        console.log("Error: ", this.todoResource.error());
    })
  }
}

The first thing we can notice is that the resource API uses Promises by default for the loader parameter. The other one is that the resource API will return a WritableResource object, which helps us to update the data locally when needed.

We can read the current value of the resource by using the value signal, the status of the resource by using the status signal, and the error of the resource by using the error signal. The code above will print the following:

Value: undefined
Status: 'loading'
Error: undefined
 
Value: { id: 1, title: "Hello World", completed: false }
Status: 'resolved'
Error: undefined

Updating the data locally

Let's see how we can update the data locally.

import { resource } from "@angular/core";
 
@Component({
    template: `
        <button (click)="updateTodo()">Update</button>
    `
})
export class MyComponent {
  todoResource = resource({
    loader: () => {
      return Promise.resolve({ id: 1, title: "Hello World", completed: false });
    },
  });
 
  updateTodo() {
    this.todoResource.value.update((value) => {
      if (!value) return undefined;
      
      return { ...value, title: "updated" };
    });
  }
}

We can update the data locally by using the update method of the value signal.

This will print the following:

Value: { id: 1, title: "updated", completed: false }
Status: 'local'
Error: undefined

The 'local' status means that the data was updated locally.

Loading the data

Let's make a proper request to the server. Let's load some todos from the JSONPlaceholder API.

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
 
@Component()
export class MyComponent {
  todosResource = resource({
    loader: () => {
      return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`)
        .then((res) => res.json() as Promise<Todo[]>);
    },
  });
}

This todosResource will start to make the request to the server immediately after it has been created.

Of course, the todosResource will not have a value yet, because the request is still in progress.

The code above will print the following:

Value: undefined
Status: 'loading'
Error: undefined
 
Value: [{ id: 1, title: "Hello World", completed: false }, { id: 2, title: "Hello World", completed: false }, ...]
Status: 'resolved'
Error: undefined

Refreshing the data Let's say we want to refresh the data when the user clicks on a button.

import { resource } from "@angular/core";
 
@Component()
export class MyComponent {
  todosResource = resource({
    loader: () => {
      return fetch(`https://jsonplaceholder.typicode.com/todos?_limit=10`)
        .then((res) => res.json() as Promise<Todo[]>);
    },
  });
 
  refresh() {
    this.todosResource.refresh();
  }
}

The refresh function will run the loader function again. If you call the refresh function multiple times, the loader function will be called only once until the previous request is finished (like exhaustMap behavior in RxJS).

Loading specific date based on other signals

Let's say we want to load the todos based on a todoId signal.

import { resource } from "@angular/core";
 
@Component()
export class MyComponent {
  todoId = signal(1);
 
  todoResource = resource({
    loader: () => {
      return fetch(
        `https://jsonplaceholder.typicode.com/todos/${this.todoId()}`
      ).then((res) => res.json() as Promise<Todo>);
    },
  });
}

This will work fine, but one this to notice is that loader is untracked and that means, that if the todoId signal changes, the load won't be called again. Let's make it more reactive!

Separate the request and the loader We want our resource to refresh the data (call the loader again) every time the todoId changes. For this we can use the request field of the resource. We can pass a signal to it, and it will be tracked.

todoResource = resource({
    request: this.todoId, 
    loader: ({ request: todoId, abortSignal }) => {  
        return fetch( 
          `https://jsonplaceholder.typicode.com/todos/${todoId}`, 
          { signal: abortSignal } 
        ).then((res) => res.json() as Promise<Todo>);
    },
});

Now, when the todoId signal changes, the resource API will automatically fetch the new data.

What if we have previous unfinished requests? Let's say we want to cancel the previous request when the todoId changes. Well, we can do that by using the abortSignal that is passed to the loader function.

todoResource = resource({
    request: this.todoId, 
    loader: ({ request: todoId, abortSignal }) => {  
        return fetch( 
          `https://jsonplaceholder.typicode.com/todos/${todoId}`, 
          { signal: abortSignal } 
        ).then((res) => res.json() as Promise<Todo>);
    },
});

This will cancel the previous request when the todoId changes and if the previous request is still in progress.

We can also have multiple request dependencies, for example:

limit = signal(10);
query = signal('');
 
todosResource = resource({
    request: () => ({ limit: this.limit(), query: this.query() }),
    loader: ({ request, abortSignal }) => {   
        const { limit, query } = request as { limit: number; query: string };
        return fetch(
          `https://jsonplaceholder.typicode.com/todos?_limit=${limit}&query=${query}`, 
          { signal: abortSignal } 
        ).then((res) => res.json() as Promise<Todo[]>);
    },
});

Now, the todosResource will make the request based on the limit and query signals, and the loader function will be able to use those signals to make the request, and anytime the limit or query signal changes, the loader function will be called again.

What happens when we have a request in progress and update data locally?

If that's the case, the resource API will automatically update the data locally, but cancel the ongoing request.

Create more reusable resources By separating reactive values from the loader function, we can move the logic of the loader function to a separate function, and store it where we want.

Before:

todoResource = resource({
    request: this.todoId,
    loader: ({ request: todoId, abortSignal }) => {  
        return fetch(
          `https://jsonplaceholder.typicode.com/todos/${todoId}`, 
          { signal: abortSignal } 
        ).then((res) => res.json() as Promise<Todo>);
    },
});

After:

import { ResourceLoaderParams } from "@angular/core";
 
function todoLoader({ request: todoId, abortSignal }: ResourceLoaderParams<number>): Promise<Todo> {
    return fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId}`, 
      { signal: abortSignal } 
    ).then((res) => res.json() as Promise<Todo>);
}
 
todoResource = resource({ request: this.todoId, loader: todoLoader });

The todoLoader can be moved anywhere, and also can be reused by other resources. The ResourceLoaderParams type is a type that contains all the information that is needed to make the request, and the request parameter is the one that is passed to the loader function.

RxResource -> The Observable based resource API

Angular has always been about using Observables when it comes to data loading. This means, that we can use Observables to derive the data loading instead of using signals & promises.

In order to make this possible, we can use the rxResource function.

import { rxResource } from "@angular/core/rxjs-interop";
 
@Component()
export class MyComponent {
  limit = signal(10);
 
  todosResource = rxResource({
    request: this.limit,
    loader: (limit) => {
      return this.http.get<Todo[]>(
        `https://jsonplaceholder.typicode.com/todos?_limit=${limit}`
      );
    },
  });
}

This will make the request based on the limit signal, and the loader function will be able to use the limit value to make the request, and same as signals, anytime the limit signal changes, the loader function will be called again and cancel the previous request (same as switchMap behavior in RxJs)

And same as we can change local state with the signals, we can also change the local state with the observables implementation in the rxResource function.

Summary

We have 2 new primitives [resource, rxResource] that will help us make our life easier when dealing with data loading in Angular. These primitives have been requested for so long now, and will land in v19 as developer previews.

PR link: https://github.com/angular/angular/pull/58255

s