DEV Community

Cover image for Angular resource() and rxResource() APIs: what you need to know
Davide Passafaro
Davide Passafaro

Posted on • Originally published at Medium

Angular resource() and rxResource() APIs: what you need to know

The release of Angular v19, just a few weeks ago, marks a significant milestone in the signal revolution within the framework, with the Input, Model, Output and Signal Queries APIs now officially promoted to stable.

But that's not all! This major version also introduces powerful new tools designed to further advance the signal revolution: the new Resource API.

As the name suggests, this new Resource API is designed to simplify loading asynchronous resources by harnessing the full power of signals!

IMPORTANT: at the time of writing, the new Resource API is still experimental. This means it may change before becoming stable, so use it at your own risk. 😅

Let's dive into how it works and how it simplifies handling async resources!


The Resource API

Most signal APIs are synchronous, but in real-world applications, it is essential to handle asynchronous resources, such as fetching data from a server or managing user interactions in real-time.

This is where the new Resource API comes into play.

Using a Resource, you can easily consume an asynchronous resource via signals, allowing you to easily manage data fetching, handle loading states, and trigger a new fetch whenever the associated signal parameters change.

resource( ) function

The easier way to create a Resource is by using the resource() function:

import { resource, signal } from '@angular/core';

const RESOURCE_URL = 'https://jsonplaceholder.typicode.com/todos/';

private id = signal(1);

private myResource = resource({
    request: () => ({ id: this.id() }),
    loader: ({ request }) => fetch(RESOURCE_URL + request.id),
});
Enter fullscreen mode Exit fullscreen mode

This function accepts a ResourceOptions configuration object as input, allowing you to specify the following properties:

  • request: a reactive function that determines the parameters used to perform the request to the asynchronous resource;
  • loader: a loading function that returns a Promise of the resource's value, optionally based on the provided request parameters. This is the only required property of ResourceOptions;
  • equal: equality function used to compare the loader's return value;
  • injector: overrides the Injector used by the Resource instance to destroy itself when the parent component or service is destroyed.

Thanks to these configurations, we can easily define an asynchronous dependency that will always be consumed efficiently and kept up-to-date.

Resource life cycle

Once a Resource is created, the loader function is executed, then the resulting asynchronous request starts:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});
console.log(myResource.status()); // Prints: 2 (which means "Loading")
Enter fullscreen mode Exit fullscreen mode

Whenever a signal that the request function depends on changes, the request function runs again, and if it returns new parameters, the loader function is triggered to fetch the updated resource's value:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 1 , ... }

id.set(2); // Triggers a request, causing the loader function to run again
console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 2 , ... }
Enter fullscreen mode Exit fullscreen mode

If no request function is provided, the loader function will run only once, unless the Resource is reloaded using the reload method (more below).

Finally, once the parent component or service is destroyed, the Resource is also destroyed unless a specific injector has been provided.

In such cases, the Resource will remain active and be destroyed only when the provided injector itself is destroyed.

Aborting requests with abortSignal

To optimize data fetching, a Resource can abort an outstanding requests if the request() computation changes while a previous value is still loading.

To manage this, the loader() function provides an abortSignal, which you can pass to ongoing requests, such as fetch. The request listens for the abortSignal and cancels the operation if it's triggered, ensuring efficient resource management and preventing unnecessary network requests:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request, abortSignal }) =>
    fetch(RESOURCE_URL + request.id, { signal: abortSignal })
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// Triggers a new request, causing the previous fetch to be aborted
// Then the loader function to run again generating a new fetch request
id.set(2);

console.log(myResource.status()); // Prints: 2 (which means "Loading")
Enter fullscreen mode Exit fullscreen mode

Based on this, it's recommended to use the Resource API primarily for GET requests, as they are typically safe to cancel without causing issues.

For POST or UPDATE requests, canceling might lead to unintended side effects, such as incomplete data submissions or updates. However, if you need similar functionality for these types of requests, you can use the effect() method to safely manage the operations.


How to consume a Resource

The Resource API provides several signal properties for its state, that you can easily use directly within your components or services:

  • value: contains the current value of the Resource, or undefined if no value is available. As a WritableSignal, it can be manually updated;
  • status: contains the current status of the Resource, indicating what the Resource is doing and what can be expected from its value;
  • error: if in the error state, it contains the most recent error raised during the Resource load;
  • isLoading: indicates whether the Resource is loading a new value or reloading the existing one.

Here's an example of how to consume a Resource within a component:

import { Component, resource, signal } from '@angular/core';

const BASE_URL = 'https://jsonplaceholder.typicode.com/todos/';

@Component({
  selector: 'my-component',
  template: `
    @if (myResource.value()) {
      {{ myResource.value().title }}
    }

    <button (click)="fetchNext()">Fetch next item</button>
  `
})
export class MyComponent {
  private id = signal(1);

  protected myResource = resource({
    request: () => ({ id: this.id() }),
    loader: ({ request }) =>
      fetch(BASE_URL + request.id).then((response) => response.json()),
  });

  protected fetchNext(): void {
    this.id.update((id) => id + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Resource is used to fetch data from an API based on the value of the id signal, which can be incremented by clicking a button.

Whenever the user clicks the button, the id signal value changes, triggering the loader function to fetch a new item from the remote API.

The UI automatically updates with the fetched data thanks to the signals properties exposed by the Resource API.


Check the status of a Resource

As mentioned earlier, the status signal provides information about the current state of the resource at any given moment.

The possible values of the status signal are defined by the ResourceStatus enum. Here's a summary of these statuses and their corresponding values:

  • Idle = 0: the Resource has no valid request and will not perform any loading. value() is undefined;
  • Error = 1: the loading has failed with an error. value() is undefined;
  • Loading = 2: the resource is currently loading a new value as a result of a change in its request. value() is undefined;
  • Reloading = 3: the resource is currently reloading a fresh value for the same request. value() will continue to return the previously fetched value until the reloading operation completes;
  • Resolved = 4: the loading is completed. value() contains the value returned from the loader data-fetching process;
  • Local = 5: the value was set locally via set() or update(). value() contains the value manually assigned.

These statuses help track the Resource's progress and facilitate better handling of asynchronous operations in your application.

hasValue( ) function

Given the complexity of these statuses, the Resource API provides a hasValue() method, which returns a boolean based on the current status.

This ensures accurate information about the Resource's status, providing a more reliable way to handle asynchronous operations without relying on the value, which might be undefined in certain states.

hasValue() {
  return (
    this.status() === ResourceStatus.Resolved ||
    this.status() === ResourceStatus.Local ||
    this.status() === ResourceStatus.Reloading
  );
}
Enter fullscreen mode Exit fullscreen mode

This method is reactive, allowing you to consume and track it like a signal.

isLoading( ) function

The Resource API also provides an isLoading signal, which returns whether the resource is currently in the Loading or Reloading state:

readonly isLoading = computed(
  () =>
    this.status() === ResourceStatus.Loading ||
    this.status() === ResourceStatus.Reloading
);
Enter fullscreen mode Exit fullscreen mode

Since isLoading is a computed signal, it can be tracked reactively, allowing you to monitor the loading state in real-time using signals APIs.


Resource value as a WritableSignal

The value signal provided by a Resource is a WritableSignal, which allows you to update it manually using the set() and update() functions:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 1 , ... }

myResource.value.set({ id: 2 });
console.log(myResource.value()); // Prints: { id: 2 }
console.log(myResource.status()); // Prints: 5 (which means "Local")

myResource.value.update((value) => ({ ...value, name: 'Davide' });
console.log(myResource.value()); // Prints: { id: 2, name: 'Davide' }
console.log(myResource.status()); // Prints: 5 (which means "Local")
Enter fullscreen mode Exit fullscreen mode

Note: as you can see, manually updating the value of the signal will also set the status to 5, which means "Local", to indicate that the value was set locally.

The manually set value will persist until either a new value is set or a new request is performed, which will override it with a new value:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 1 , ... }

myResource.value.set({ id: 2 });
console.log(myResource.value()); // Prints: { id: 2 }
console.log(myResource.status()); // Prints: 5 (which means "Local")

id.set(3); // Triggers a request, causing the loader function to run again
console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 3 , ... }
Enter fullscreen mode Exit fullscreen mode

Note: the value signal of the Resource API uses the same pattern of the new LinkedSignal API, but does not use it under the hood. 🤓

Convenience wrappers methods

To simplify the use of the value signal, the Resource API provides convenience wrappers for the set, update, and asReadonly methods.

The asReadonly method is particularly useful as it returns a read-only instance of the value signal, allowing access only for reading and preventing any accidental modifications.

You can use this approach to create services that manage and track changes to resource values by exporting a read-only instance of the value:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

export class MyService {
  const id = signal(1);

  const myResource = resource({
    request: () => ({ id: id() }),
    loader: ({ request }) => fetch(RESOURCE_URL + request.id })
  });

  public myValue = myResource.value.asReadonly();

  setValue(newValue) {
    // Wrapper of `myResource.value.set()`
    myResource.set(newValue);
  }

  addToValue(addToValue) {
    // Wrapper of `myResource.value.update()`
    myResource.update((value) => ({ ...value, ...addToValue });
  }
}

// Usage of the service in a component or other part of the application
const myService = new MyService();

myService.myValue.set(null); // Property 'set' does not exist in type 'Signal'

myService.setValue({ id: 2 });
console.log(myService.myValue()); // Prints: { id: 2 }

myService.addToValue({ name: 'Davide' });
console.log(myService.myValue()); // Prints: { id: 2, name: 'Davide' }
Enter fullscreen mode Exit fullscreen mode

This will prevent consumers from modifying the value, reducing the risk of unintended changes, improving consistency in complex data management.


Reload or destroy a Resource

When working with asynchronous resources, you may face scenarios where refreshing the data or destroy the Resource becomes necessary.

To handle these scenarios, the Resource API provides two dedicated methods that offer efficient solutions for managing these actions.

reload( ) function

The reload() method instructs the Resource to re-execute the asynchronous request, ensuring it fetches the most up-to-date data:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")

myResource.reload(); // Returns true if a reload was initiated
console.log(myResource.status()); // Prints: 3 (which means "Reloading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 5 (which means "Local")
Enter fullscreen mode Exit fullscreen mode

The reload() method returns true if a reload is successfully initiated.

If a reload cannot be performed, either because it is unnecessary, such as when the status is already Loading or Reloading, or unsupported, like when the status is Idle, the method returns false.

destroy( ) function

The destroy() method manually destroys the Resource, destroying any effect() used to track request changes, canceling any pending requests, and setting the status to Idle while resetting the value to undefined:

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

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = resource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")

myResource.destroy(); // Returns true if a reload was initiated
console.log(myResource.status()); // Prints: 1 (which means "Idle")
console.log(myResource.value()); // Prints: undefined
Enter fullscreen mode Exit fullscreen mode

After a Resource is destroyed, it will no longer respond to request changes or reload() operations.

Note: at this point, while the value signal remains writable, the Resource will lose its intended purpose and no longer serves its function, becoming useless. 🙃


rxResource( ) function

Like nearly all signal-based APIs introduced so far, the Resource API also offers an interoperability utility for seamless integration with RxJS.

Instead of using the resource() method to create a Promise-based Resource, you can use the rxResource() method to use Observables:

import { resource, signal } from "@angular/core";
import { rxResource } from '@angular/core/rxjs-interop';
import { fromFetch } from 'rxjs/fetch';

const RESOURCE_URL = "https://jsonplaceholder.typicode.com/todos/";

const id = signal(1);
const myResource = rxResource({
  request: () => ({ id: id() }),
  loader: ({ request }) => fromFetch(RESOURCE_URL + request.id)
});

console.log(myResource.status()); // Prints: 2 (which means "Loading")

// After the fetch resolves

console.log(myResource.status()); // Prints: 4 (which means "Resolved")
console.log(myResource.value()); // Prints: { "id": 1 , ... }
Enter fullscreen mode Exit fullscreen mode

Note: the rxResource() method is in fact exposed by the rxjs-interop package.

The Observable produced by the loader() function will consider only the first emitted value, ignoring subsequent emissions.


Thanks for reading so far 🙏

Thank you all for following me throughout this wonderful 2024. 🫶🏻

It has been a year full of challenges, but also very rewarding. I have big plans for 2025 and I can't wait to start working on them. 🤩

I’d like to have your feedback so please leave a comment, like or follow. 👏

Then, if you really liked it, share it among your community, tech bros and whoever you want. And don’t forget to follow me on LinkedIn. 👋😁

Top comments (0)