skip to content

Async Resources

resource() turns reactive params into async state through a loader that receives an abort signal, and rxResource does it from an Observable. Interviewers probe status, reloads and stale requests.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular, how does resource() turn a selected user id signal into loaded order history, and what do its params and loader options do?

level: juniorimportance: must knowfreq 52%

answer

  1. reactive request, async result
  2. params is tracked like computed
  3. loader receives params and abortSignal
  4. undefined params means idle
  5. loader body runs untracked

basics

~20 s

resource() pairs a reactive params function with an async loader: whenever the signals read in params produce a new value, Angular calls the loader with it and exposes the result through signals such as value(), status() and isLoading().

solid answer

~40 s

`resource()` from `@angular/core` takes a `params` function and a `loader`. `params` is tracked like a `computed`: it reads, say, `selectedUserId()` and returns the request value. Each time it produces a new value, the resource switches to `'loading'` and calls the `loader` with `{params, abortSignal, previous}`; the loader returns a promise, and its result lands in `value()` with status `'resolved'`. If `params` returns `undefined`, the loader does not run and the status is `'idle'` — that is how you model "no user selected". Only `params` is reactive: signals read inside the loader are not tracked. The resource must be created in an injection context (or be given an `injector`) and is torn down with that injector. It is stable public API in Angular 22.

code

ts · 38 lines
ts
import { Component, input, resource } from '@angular/core';

interface Order {
  id: string;
  placedAt: string;
  total: number;
}

@Component({
  selector: 'app-order-history',
  template: `
    @if (orders.isLoading()) {
      <p>Loading orders...</p>
    } @else if (orders.error()) {
      <p>Could not load orders.</p>
    } @else if (orders.hasValue()) {
      <ul>
        @for (order of orders.value(); track order.id) {
          <li>{{ order.placedAt }}: {{ order.total }}</li>
        }
      </ul>
    }
  `,
})
export class OrderHistory {
  readonly userId = input<string | undefined>();

  readonly orders = resource({
    params: () => this.userId(),
    loader: async ({ params: id, abortSignal }) => {
      const res = await fetch(`/api/users/${id}/orders`, { signal: abortSignal });
      if (!res.ok) {
        throw new Error(`Orders request failed: ${res.status}`);
      }
      return (await res.json()) as Order[];
    },
  });
}

go deeper

for a junior

Recall the two options: params says what to load and is reactive, loader does the async work. Know that undefined params keeps the resource idle.

for a middle

Explain that only params is tracked, that the loader gets params, abortSignal and previous, and how value() resets when params change.

for a senior

Show how you shape params so idle, loading and error states fall out naturally, and where the resource is created so it is torn down with its component.

for a principal

Weigh where async data loading belongs in a team codebase: resources in components, in services, or behind a shared data layer, and what each does to testing.

## What `resource()` is for Angular's signal primitives — `signal()`, `computed()`, `effect()` — are all **synchronous**. A `computed()` cannot `await` a server response. Yet most screens depend on data that arrives later: the order history of whichever customer is selected in a list, for example. `resource()`, exported from `@angular/core`, is the bridge. It takes a reactive description of *what to load* and an async function describing *how to load it*, and it hands the result back as **signals** you can read synchronously in templates, `computed()` derivations and effects. The function was introduced as experimental in Angular 19 and is tagged as stable public API (`@publicApi 22.0`) in Angular 22, the version this answer assumes. ## The two halves: `params` and `loader` A resource is configured with an options object whose two central properties split the work cleanly: - **`params`** — a function that reads signals and returns a request value. Angular tracks it the way it tracks a `computed()`: every signal read inside it becomes a dependency. In the order-history case it is usually just `() => this.userId()`. - **`loader`** — an async function (it returns a `PromiseLike`) that receives a single `ResourceLoaderParams` object and fetches the data. The loader's argument carries three properties: | Property | What it holds | | --- | --- | | `params` | The current value produced by the `params` function, with `undefined` excluded from its type | | `abortSignal` | An `AbortSignal` Angular fires when this load is superseded or the resource is destroyed | | `previous` | An object whose `status` is the resource's previous `ResourceStatus` | The lifecycle, step by step: 1. The component creates the resource; Angular evaluates `params`. 2. If the value is defined, the status becomes `'loading'` and an internal effect calls the loader. 3. The loader's promise resolves; the result is written to `value()` and the status becomes `'resolved'`. A rejection sets the status to `'error'` and fills `error()`. 4. The user picks another customer; `params` produces a new value, the status flips back to `'loading'` at once, `value()` drops back to `undefined` (or the `defaultValue`), and the loader runs again for the new id. ## Only `params` is reactive Angular calls the loader inside `untracked`. A signal read in the loader body — a currency, a page size, a feature flag — does **not** become a dependency, and changing it later triggers nothing. Anything that should cause a new load belongs in `params` and travels to the loader through `params`. The rule keeps reactivity on one side of the `await`, where tracking is predictable. ## Idle: undefined params versus no params at all - When `params` **returns `undefined`**, the loader does not run and `status()` is `'idle'`. That is the idiomatic way to say "nothing selected yet". - Returning an object such as `{id: userId()}` defeats this: the object is defined even when `id` is not, so the loader runs with an undefined id. Return `undefined` itself. - When the `params` option is **omitted**, the loader runs once and runs again only when you call `reload()`. ## What you read back - `value()` — the loaded data, typed `T | undefined` unless you pass `defaultValue`. - `status()` — one of `'idle'`, `'loading'`, `'reloading'`, `'resolved'`, `'error'` or `'local'`. - `isLoading()` — `true` while loading or reloading. - `error()` — the last `Error`, or `undefined`. - `hasValue()` — a reactive check that also narrows the type, and the safe guard before reading `value()`. ## Where it can live `resource()` builds on `effect()` and `DestroyRef`, so it must be created in an **injection context** — a field initializer or constructor of a component, directive or service — or be given an explicit `injector` option. When that injector is destroyed, for example when the component leaves the page, the resource aborts any in-flight load and returns to `'idle'`. ```ts readonly orders = resource({ params: () => this.userId(), // undefined -> 'idle' loader: ({ params: id, abortSignal }) => fetch(`/api/users/${id}/orders`, { signal: abortSignal }) .then((res) => res.json() as Promise<Order[]>), }); ``` ## Common mistakes - Expecting the loader to rerun when a signal read inside it changes. - Wrapping an undefined id in an object and loading `/users/undefined/orders`. - Creating the resource inside a click handler with no `injector` option. - Treating `resource()` as a general async helper for writes; it is designed for reads.

  • What happens if params returns { id: userId() } while no user is selected?
    The object is defined even though `id` is not, so the resource does not go idle: the loader runs with `id` undefined and requests something like `/users/undefined/orders`. Return `undefined` from `params` itself — `() => this.userId()` — or `userId() ? {id: userId()} : undefined`.
  • Why does changing a signal that the loader reads not start a new load?
    Angular runs the loader through `untracked`, so only the `params` function builds dependencies. That keeps tracking on one side of the `await`. A value that should trigger a reload has to be read in `params` and passed to the loader as part of the params value.
  • Can you create a resource inside a button click handler?
    Not as written: outside an injection context `resource()` fails the dev-mode injection-context assertion, because it needs an injector for its internal effect and its `DestroyRef`. Pass the `injector` option explicitly, or better, create the resource in a field initializer and drive it through a signal the click handler sets.

saying these in an interview costs you the question

  • The loader reruns whenever any signal it reads changes.
  • Returning undefined from params calls the loader with undefined.
  • resource() needs a dependency array listing the signals it watches.
  • The previous user's orders stay in value() while the next user loads.
  • resource() can be created anywhere, just like a plain signal().
open as a page

In Angular, what does each resource status value mean, and what does value() return in each state, including after the loader fails?

level: middleimportance: must knowfreq 50%

basics

~20 s

A resource reports idle, loading, reloading, resolved, error or local. value() is undefined (or defaultValue) when idle or loading, keeps the old data while reloading, and throws in the error state, so guard reads with hasValue().

open as a page

In Angular, how does rxResource() differ from resource() when the order-history service you call already returns an Observable?

level: middleimportance: should knowfreq 42%

basics

~20 s

rxResource() from @angular/core/rxjs-interop takes a stream function returning an Observable instead of a promise loader; it subscribes per params value, updates value() on every emission and unsubscribes when params change, while exposing the same Resource API.

open as a page

When an Angular resource() loading order history switches users mid-request, what happens to the old response, and why still pass abortSignal to the loader?

level: seniorimportance: should knowfreq 38%

basics

~10 s

Angular aborts the superseded load and discards its result, so a slow response for the previous user never overwrites the new one. Passing abortSignal to fetch lets that abort cancel the network request itself.

open as a page

In Angular 22, how does debounced() delay a filter signal before it feeds a resource(), and what does it report while waiting?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

debounced(source, wait) returns a read-only Resource whose value() is the last settled value of the source signal; while the wait runs it reports 'loading' and keeps the old value, so a resource reading it in params only reloads once input settles.

open as a page