DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Async Reactivity with Angular Resources — A Production‑Minded Guide (2026)

Async Reactivity with Angular Resources — A Production‑Minded Guide (2026)

Async Reactivity with Angular Resources — A Production‑Minded Guide (2026)

Signals are synchronous by design (signal, computed, effect, input).

But real apps aren’t. They fetch data, debounce queries, cancel requests, retry, and render loading/error states.

Resource is Angular’s answer: async data that plugs into signal-based code while keeping template reads synchronous.

⚠️ Important: resource is currently experimental. It’s ready to try, but the API may change before it’s stable.


Table of Contents


The Problem Resource Solves

Traditional Angular async patterns typically look like:

  • Observable pipelines (RxJS)
  • async pipes in templates
  • manual subscription management in services/components
  • “loading” booleans scattered across state

Resources centralize the async lifecycle into a single ref, while exposing:

  • value() (signal)
  • status() (signal)
  • isLoading() (signal)
  • error() (signal)
  • hasValue() (type guard)

Result: UI code becomes deterministic.


Core Mental Model

Think of a Resource as:

  1. A reactive input: params()
  2. A side-effectful async function: loader({ params, previous, abortSignal })
  3. A signal output: value() plus status signals

It behaves like a state machine:

idle → loading → resolved
   ↘︎ error
resolved → reloading → resolved
resolved → local (if you set/update locally)
Enter fullscreen mode Exit fullscreen mode

Resource Anatomy: params + loader

The simplest form uses resource({...}):

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

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

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

const firstName = computed(() => {
  if (userResource.hasValue()) {
    return userResource.value().firstName;
  }
  return undefined;
});
Enter fullscreen mode Exit fullscreen mode

What params() really means

  • params is a reactive computation
  • every signal read inside it becomes a dependency
  • when any dependency changes, Angular recomputes params and triggers a (new) load

If params() returns undefined, the loader will not run and the resource becomes idle.


Resource Status: build UIs without guesswork

Resources provide status signals:

  • value() → last resolved value (or undefined)
  • hasValue() → type guard + safe read check
  • error() → last error (or undefined)
  • isLoading() → whether loader is running
  • status() → fine-grained state machine string

Status values:

Status value() Meaning
idle undefined No valid params; loader hasn’t run
loading undefined Loader running due to params change
reloading previous value Loader running because .reload()
resolved resolved value Loader completed successfully
error undefined Loader threw/failed
local locally set value You used .set() / .update()

Template pattern (clean and predictable)

@if (user.status() === 'loading') {
  <app-skeleton />
} @else if (user.status() === 'error') {
  <app-error [error]="user.error()" />
} @else if (user.hasValue()) {
  <app-user-card [user]="user.value()" />
} @else {
  <p>No user loaded.</p>
}
Enter fullscreen mode Exit fullscreen mode

This avoids “undefined gymnastics” and makes UI behavior explicit.


Cancellation (AbortSignal) done right

A resource aborts outstanding loads if params() changes mid-flight.

Angular exposes an AbortSignal:

const userResource = resource({
  params: () => ({ id: userId() }),
  loader: ({ params, abortSignal }): Promise<User> => {
    return fetch(`/users/${params.id}`, { signal: abortSignal })
      .then(r => r.json());
  },
});
Enter fullscreen mode Exit fullscreen mode

Why you should care (production)

Cancellation prevents:

  • race conditions (slow response overwriting the newest)
  • wasted bandwidth
  • stale UI updates
  • “double renders” after rapid navigation or input changes

Reloading: deterministic refresh

Resources can be refreshed without changing parameters:

userResource.reload();
Enter fullscreen mode Exit fullscreen mode

Key detail: reload transitions to reloading (preserving value()), which is ideal for:

  • background refresh
  • pull-to-refresh
  • “Retry” buttons that don’t wipe the screen

Patterns You’ll Use in Production

1) Parameterized fetch (route/query)

import { resource } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs/operators';

const route = inject(ActivatedRoute);

// Convert paramMap observable to signal
const userId = toSignal(
  route.paramMap.pipe(map(m => m.get('id') ?? '')),
  { initialValue: '' }
);

const user = resource({
  params: () => userId() ? ({ id: userId() }) : undefined,
  loader: ({ params, abortSignal }) =>
    fetch(`/api/users/${params.id}`, { signal: abortSignal }).then(r => r.json()),
});
Enter fullscreen mode Exit fullscreen mode

Note: returning undefined cleanly switches the resource to idle.


2) Search with debounce + cancellation

Signals are synchronous; debouncing is time-based.

A clean approach is: Signal input → RxJS debounce → back to Signal.

import { signal, resource } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';

const q = signal('');

const debouncedQ = toSignal(
  toObservable(q).pipe(
    map(x => x.trim()),
    debounceTime(250),
    distinctUntilChanged(),
  ),
  { initialValue: '' }
);

const results = resource({
  params: () => debouncedQ() ? ({ q: debouncedQ() }) : undefined,
  loader: async ({ params, abortSignal }) => {
    const r = await fetch(`/api/search?q=${encodeURIComponent(params.q)}`, { signal: abortSignal });
    if (!r.ok) throw new Error(`Search failed: ${r.status}`);
    return r.json() as Promise<{ items: any[] }>;
  },
});
Enter fullscreen mode Exit fullscreen mode

3) Optimistic local state: local status

Resources support local updates:

  • .set(value)
  • .update(fn)

When you do that, status becomes local.

// optimistic update
user.set({ ...user.value(), displayName: nextName });

// background refresh
user.reload();
Enter fullscreen mode Exit fullscreen mode

4) Error boundaries + fallbacks

A safe computed pattern:

import { computed } from '@angular/core';

const safeUserName = computed(() => {
  if (!user.hasValue()) return 'Unknown';
  return user.value().displayName ?? 'Unknown';
});
Enter fullscreen mode Exit fullscreen mode

Rule: never assume value() exists unless guarded by hasValue() or status checks.


httpResource: HttpClient as signals

httpResource wraps HttpClient and returns:

  • request status as signals
  • response as signals
  • works through Angular HTTP stack (interceptors, auth, logging)

Use it when:

  • you rely on interceptors (auth tokens, retries, headers)
  • you want consistent HttpClient behavior (vs raw fetch)
  • you want request lifecycle without manual Subject/BehaviorSubject scaffolding

Exact import path and API shape can vary by Angular version because this area is evolving.


Resource vs RxJS vs rxResource

Use Signals when:

  • state is local/UI-facing
  • derived state matters (computed)
  • you want predictable “pull-based” reads

Use RxJS when:

  • events/time matter (debounce, merge, retry, websockets)
  • you need operators like switchMap, combineLatest, shareReplay

Use Resource when:

  • you need async data as signals
  • you want cancellation/status built-in
  • you want deterministic UI logic without custom loading/error flags

Use rxResource when:

  • your async source is naturally an RxJS stream
  • you want Resource lifecycle + RxJS composition
  • you’re already in the RxJS ecosystem and want the Resource ref type

Angular v20+ note: If you’re migrating rxResource, options renamed:
request → params, loader → stream. If you still pass request, TypeScript will error.


Testing Resources

Test:

  • status transitions (loading → resolved, loading → error, reloading)
  • cancellation when params change quickly
  • UI guards (hasValue() and status())

Pseudo-example:

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

it('goes to resolved when loader completes', async () => {
  const id = signal('1');
  const user = resource({
    params: () => ({ id: id() }),
    loader: async () => ({ name: 'Ada' }),
  });

  expect(user.status()).toBe('loading');
  await Promise.resolve();
  expect(user.status()).toBe('resolved');
  expect(user.value().name).toBe('Ada');
});
Enter fullscreen mode Exit fullscreen mode

SSR notes and PendingTasks

SSR needs to know when your app is “stable” to produce the final HTML.
Modern Angular trends toward explicit pending task tracking (vs ZoneJS stability heuristics).

Practical guidance:

  • keep async loads in Resources
  • ensure SSR runtime can await those pending operations
  • prefer Angular-provided primitives (PendingTasks) where available

Pitfalls & Anti‑Patterns

  • Async inside computed() → computed must be pure & synchronous
  • Ignoring AbortSignal → invites race conditions
  • Reading value() without guards → causes errors in loading/error/idle
  • Forcing websocket streams into Resource → Resources are request lifecycle, not infinite streams
  • Noisy params (whitespace, fast typing) → normalize + debounce before the resource

Upgrade & Safety Checklist

  • ✅ Treat Resource as experimental: isolate behind a service boundary.
  • ✅ Always use AbortSignal when supported.
  • ✅ Drive UI from status() instead of ad-hoc booleans.
  • ✅ Return undefined from params() for “no request”.
  • ✅ Use immutable updates (set({...}), set([...])), don’t mutate nested objects.
  • ✅ Use .reload() for retry/refresh (preserves value on reloading).
  • ✅ Measure: cancellation should eliminate stale response bugs.

Conclusion

Resources bring a missing piece to signal-first Angular:

  • async data with sync reads
  • built-in cancellation
  • status-driven UIs
  • fewer custom “loading/error flags” across components

If you paste your current resource({ ... }) block, I’ll rewrite it into a production-ready version with:

  • strong types
  • cancellation
  • guarded template access
  • ergonomic status UI

✍️ Cristian Sifuentes

Building scalable Angular + .NET systems, obsessed with clean reactivity, performance, and production ergonomics.

Top comments (0)