Back to Angular

Async reactivity with resources

adev/src/content/guide/signals/resource.md

22.1.0-next.312.4 KB
Original Source

Async reactivity with resources

All signal APIs are synchronous— signal, computed, input, etc. However, applications often need to deal with data that is available asynchronously. A Resource gives you a way to incorporate async data into your application's signal-based code and still allow you to access its data synchronously.

You can use a Resource to perform any kind of async operation, but the most common use-case for Resource is fetching data from a server. The following example creates a resource to fetch some user data.

The easiest way to create a Resource is the resource function.

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

const userId: Signal<string> = getUserId();

const userResource = resource({
  // Define a reactive computation.
  // The params value recomputes whenever any read signals change.
  params: () => ({id: userId()}),

  // Define an async loader that retrieves data.
  // The resource calls this function every time the `params` value changes.
  loader: ({params}) => fetchUser(params),
});

// Create a computed signal based on the result of the resource's loader function.
const firstName = computed(() => {
  if (userResource.hasValue()) {
    // `hasValue` serves 2 purposes:
    // - It acts as type guard to strip `undefined` from the type
    // - It protects against reading a throwing `value` when the resource is in error state
    return userResource.value().firstName;
  }

  // fallback in case the resource value is `undefined` or if the resource is in error state
  return undefined;
});

The resource function accepts a ResourceOptions object with two main properties: params and loader.

The params property defines a reactive computation that produces a parameter value. Whenever signals read in this computation change, the resource produces a new parameter value, similar to computed.

The loader property defines a ResourceLoader— an async function that retrieves some state. The resource calls the loader every time the params computation produces a new value, passing that value to the loader. See Resource loaders below for more details.

Resource has a value signal that contains the results of the loader.

Resource loaders

When creating a resource, you specify a ResourceLoader. This loader is an async function that accepts a single parameter— a ResourceLoaderParams object— and returns a value.

The ResourceLoaderParams object contains three properties: params, previous, and abortSignal.

PropertyDescription
paramsThe value of the resource's params computation.
previousAn object with a status property, containing the previous ResourceStatus.
abortSignalAn AbortSignal. See Aborting requests below for details.

If the params computation returns undefined, the loader function does not run and the resource status becomes 'idle'.

Aborting requests

A resource aborts an outstanding loading operation if the params computation changes while the resource is loading.

You can use the abortSignal in ResourceLoaderParams to respond to aborted requests. For example, the native fetch function accepts an AbortSignal:

typescript
const userId: Signal<string> = getUserId();

const userResource = resource({
  params: () => ({id: userId()}),
  loader: ({params, abortSignal}): Promise<User> => {
    // fetch cancels any outstanding HTTP requests when the given `AbortSignal`
    // indicates that the request has been aborted.
    return fetch(`users/${params.id}`, {signal: abortSignal});
  },
});

See AbortSignal on MDN for more details on request cancellation with AbortSignal.

Reloading

You can programmatically trigger a resource's loader by calling the reload method.

typescript
const userId: Signal<string> = getUserId();

const userResource = resource({
  params: () => ({id: userId()}),
  loader: ({params}) => fetchUser(params),
});

// ...

userResource.reload();

Resource status

The resource object has several signal properties for reading the status of the asynchronous loader.

PropertyDescription
valueThe most recent value of the resource, or undefined if no value has been received.
hasValueWhether the resource has a value.
errorThe most recent error encountered while running the resource's loader, or undefined if no error has occurred.
isLoadingWhether the resource loader is currently running.
statusThe resource's specific ResourceStatus, as described below.

The status signal provides a specific ResourceStatus that describes the state of the resource using a string constant.

Statusvalue()Description
'idle'undefinedThe resource has no valid request and the loader has not run.
'error'undefinedThe loader has encountered an error.
'loading'undefinedThe loader is running as a result of the params value changing.
'reloading'Previous valueThe loader is running as a result of calling the resource's reload method.
'resolved'Resolved valueThe loader has completed.
'local'Locally set valueThe resource's value has been set locally via .set() or .update()

You can use this status information to conditionally display user interface elements, such as loading indicators and error messages.

Caching resource data with SSR

When an application renders on the server, a resource loader runs once to produce the initial HTML. During hydration, the browser normally runs the same loader again.

To reuse the server result, provide an id for the resource. Angular stores the resolved value in TransferState on the server and uses it on the client to initialize the resource in a 'resolved' state.

ts
const userId: Signal<string> = getUserId();

const userResource = resource({
  params: () => ({id: userId()}),
  loader: ({params}) => fetchUser(params),
  id: 'user-unique-id',
});

The id value must be unique within your application and identical on the server and the client so that Angular can match the cached entry to the resource that requested it.

IMPORTANT: Because the cached value is serialized into the page's HTML, avoid setting id on resources that load data specific to the user who triggered the server-side render, especially if the rendered HTML can be cached or shared between users.

Chaining resources

Sometimes one resource depends on the result of another. You can express this dependency using the chain function available in the params context object.

typescript
import {resource} from '@angular/core';

const userResource = resource({
  params: () => ({id: getUserId()}),
  loader: ({params}) => fetchUser(params),
});

const companyResource = resource({
  params: ({chain}) => chain(userResource)?.companyId,
  loader: ({params: companyId}) => fetchCompany(companyId),
});

Here companyResource depends on the user's companyId, which is only known once userResource has loaded. chain(userResource) reads the value of userResource and automatically propagates its status to companyResource:

  • If userResource is idle, companyResource also becomes idle.
  • If userResource is loading or reloading, companyResource enters the loading state and its loader does not run. Note that during reloading, chain does not return the previously resolved value.
  • If userResource is in an error state, companyResource also enters the error state.
  • If userResource is resolved or local, chain returns its current value, which companyResource then uses as its params.

When chain propagates a status from userResource (idle, loading, reloading, or error), the params function does not continue. When userResource is resolved or local, chain returns its value, which can itself be undefined. The example handles this with chain(userResource)?.companyId, so an undefined value results in undefined params and companyResource becomes idle.

NOTE: Pass the chained value directly as the params value rather than wrapping it in an object. A params value like {companyId: undefined} is still a defined value, so the loader would run with an undefined companyId instead of the resource becoming idle.

Chaining vs. reading resource values directly

You might be tempted to read a resource's value directly inside params:

typescript
const companyResource = resource({
  params: () => {
    const user = userResource.value(); // may be undefined
    return user ? {companyId: user.companyId} : undefined;
  },
  loader: ({params}) => fetchCompany(params.companyId),
});

While this works, returning undefined from params makes the resource go idle rather than reflecting the actual state of the upstream resource. Using chain is preferred because it correctly mirrors loading and error states.

Reach for chain only when the downstream resource performs its own async work that depends on the upstream value. If you only need to derive a value synchronously from a resource, use computed instead.

Reactive data fetching with httpResource

httpResource is a wrapper around HttpClient that gives you the request status and response as signals. It makes HTTP requests through the Angular HTTP stack, including interceptors.

Resource composition with snapshots

A ResourceSnapshot is a structured representation of a resource's current state. Every resource has a snapshot property that provides a signal of its current state.

ts
const userId: Signal<string> = getUserId();

const userResource = resource({
  params: () => ({id: userId()}),
  loader: ({params}) => fetchUser(params),
});

const userSnapshot = userResource.snapshot;

Each snapshot contains a status and either a value or an error.

Composing resources with snapshots

You can create new resources from snapshots using resourceFromSnapshots. This enables composition with signal APIs like computed and linkedSignal to transform resource behavior.

ts
import {linkedSignal, resourceFromSnapshots, Resource, ResourceSnapshot} from '@angular/core';

function withPreviousValue<T>(input: Resource<T>): Resource<T> {
  const derived = linkedSignal<ResourceSnapshot<T>, ResourceSnapshot<T>>({
    source: input.snapshot,
    computation: (snap, previous) => {
      if (snap.status === 'loading' && previous && previous.value.status !== 'error') {
        // When the input resource enters loading state, we keep the value
        // from its previous state, if any.
        return {status: 'loading' as const, value: previous.value.value};
      }

      // Otherwise we simply forward the state of the input resource.
      return snap;
    },
  });

  return resourceFromSnapshots(derived);
}

@Component({
  /*... */
})
export class AwesomeProfile {
  userId = input.required<number>();
  user = withPreviousValue(httpResource(() => `/user/${this.userId()}`));
  // When userId changes, user.value() keeps the old user data until the new one loads
}