skip to content

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().