1. toSignal() & toObservable()
You've used toSignal() since Week 7 without a full explanation — here it
is. It subscribes to an Observable on your behalf and exposes the latest emitted value
as a signal, unsubscribing automatically when the injection context is destroyed.
import { Component, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
export class ProductDetail {
private route = inject(ActivatedRoute);
productId = toSignal(
this.route.paramMap.pipe(map((params) => params.get('id'))),
{ initialValue: null }
);
}
toObservable() runs the other direction — it turns a signal into an
Observable, useful whenever you need to feed a signal's changing value into an RxJS
pipeline (debouncing a signal-backed search term before an HTTP call, for instance):
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
export class LiveSearch {
searchTerm = signal('');
results$ = toObservable(this.searchTerm).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((term) => this.search(term))
);
private search(term: string) {
return this.http.get<Result[]>(`/api/search?q=${term}`);
}
}
toObservable() is synchronous-to-async by nature: because signal reads are
synchronous, it needs at least one change-detection cycle to notice the signal changed —
it uses an effect internally to bridge the two worlds, which is worth knowing if timing
ever looks slightly delayed compared to reading the signal directly.
2. resource() for Async Data Loading
resource() is a signal-first API for loading async data — the modern
alternative to manually managing loading/error/data signals around an HTTP call.
You give it a request (any signal-derived value) and a
loader function; it re-runs the loader automatically whenever the request
changes.
import { Component, signal, resource } from '@angular/core';
export class UserProfile {
userId = signal(1);
user = resource({
request: () => ({ id: this.userId() }),
loader: async ({ request, abortSignal }) => {
const res = await fetch(`/api/users/${request.id}`, { signal: abortSignal });
if (!res.ok) throw new Error('Failed to load user');
return res.json();
},
});
}
@if (user.isLoading()) {
<app-spinner />
} @else if (user.error()) {
<p>Couldn't load this user.</p>
} @else if (user.value(); as u) {
<h1>{{ u.name }}</h1>
}
Change userId, and resource() automatically cancels any
in-flight load (via the abortSignal it hands your loader) and starts a new
one — the same cancellation behavior switchMap gave you in Week 15, without
writing any RxJS at all.
3. rxResource() — the Observable-Based Variant
Most of your data layer (Week 14) already returns Observables from
HttpClient — rxResource() is resource()'s sibling
for exactly that case, taking a loader that returns an Observable instead of a Promise.
import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { ProductsService } from './products.service';
export class ProductDetail {
private products = inject(ProductsService);
productId = signal('42');
product = rxResource({
request: () => ({ id: this.productId() }),
loader: ({ request }) => this.products.getById(request.id),
});
}
Same .value(), .isLoading(), .error() API as
resource() — the only difference is what kind of function you hand the
loader. In practice, reach for rxResource() whenever your data
layer already speaks Observables (which, after Week 14, is most of your app), and
resource() for anything Promise-based, like a raw fetch() call
or a non-Angular SDK.
4. Patterns for "Signal-First" Component Design
With resource()/rxResource() in hand, a component's entire
data flow can be expressed as signals end to end — inputs, derived state, and now async
data too — with RxJS pushed down into services as an implementation detail rather than
something every component juggles directly.
export class OrderHistory {
// Signal in: route param, already bridged by withComponentInputBinding (Week 10)
userId = input.required<string>();
// Signal derived: request shape for the resource
private request = computed(() => ({ userId: this.userId(), page: this.page() }));
page = signal(1);
// Signal out: async data, loading, and error all from one resource
orders = rxResource({
request: this.request,
loader: ({ request }) => this.ordersService.getPage(request.userId, request.page),
});
// Signal derived: from the resource's own value
hasOrders = computed(() => (this.orders.value()?.length ?? 0) > 0);
}
Nothing here is a Promise, a Subject, or a manual subscription — the whole component is readable top to bottom as "signals in, signals out," which is exactly the design goal this pattern is named for.
5. Handling Loading/Error/Success States with resource()
A resource() exposes more than just .value() — its
.status() signal gives you the exact current state, useful when
isLoading()/error() alone don't capture enough nuance (like
distinguishing a fresh load from a reload of already-visible data).
// resource().status() is one of:
// 'idle' -- no request yet (request() returned undefined)
// 'loading' -- first load in progress, no value yet
// 'reloading' -- request changed, previous value still available while the new one loads
// 'resolved' -- loader succeeded, value() is populated
// 'error' -- loader threw, error() is populated
// 'local' -- value was set manually via resource.set(), not from the loader
@if (orders.status() === 'loading') {
<app-spinner />
} @else {
@if (orders.status() === 'reloading') {
<div class="stale-banner">Refreshing…</div>
}
@for (order of orders.value(); track order.id) {
<app-order-row [order]="order" />
}
}
This is the pattern that makes pagination feel smooth — the previous page's data stays
on screen (rather than flashing to a blank loading state) while
reloading fetches the next page underneath it.
6. Avoiding Common Interop Bugs
Two mistakes account for most signal/RxJS interop bugs in practice.
// WRONG: captures the value of count() at effect-creation time, never updates
const count = signal(0);
const currentCount = count(); // reads it ONCE, right now
effect(() => {
console.log(currentCount); // always logs the original value, forever
});
// RIGHT: read the signal INSIDE the effect, so it's tracked
effect(() => {
console.log(count()); // re-runs every time count() actually changes
});
The fix is always the same: call the signal inside the reactive context
(effect, computed, a template) rather than storing its current
value in a plain variable beforehand.
// WRONG: subscribes twice, so the HTTP request fires TWICE
export class ProductList {
products$ = this.http.get<Product[]>('/api/products');
ngOnInit() {
this.products$.subscribe((products) => console.log(products.length)); // subscription #1
}
}