Goal I'm using Angular with the new signals feature and the experimental HttpResource. I want to:
Extract an id parameter from the route. Bind this id to a signal. Use that signal to fetch product details from an API whenever the value changes.
What I Tried
export class ProductDetailComponent implements OnInit {
private _route = inject(ActivatedRoute);
readonly productId = computed(() => {
return Number(this._route.snapshot.paramMap.get('productId'));
});
readonly productResource = computed(() => {
const id = this.productId();
if (!id) return null;
return httpResource<any>({
url: `product/${id}`,
method: 'GET'
});
});
readonly product = computed(() => this.productResource()?.value());
}
What I Expected
Read the productId from the URL using Angular Router.
Track it with a computed signal.
Fetch product details automatically when the productId is available or changes.
We can use toSignal
to convert any observable
to a signal
, we then use map
to transform the string productId to a number
.
readonly productId = toSignal<number>(
this._route.params.pipe(map((res: Params) => +res['productId']))
);
Once we have the productId
signal, we can easily call the httpResource
, this is an initializer API, so you can just initialize it once at the root of the class, no need to wrap it inside a computed
or method
.
readonly productResource = httpResource<any>(
() => `https://jsonplaceholder.typicode.com/todos/${this.productId()}`
);
The httpResource
accepts a callback, since you are just making a GET
call (default mode), just specify the url as the return value to the callback.
The signals used inside the URL will be used to trigger the reactivity, when the signals change, the URL is fetched again.
Then we use computed
to get the value from the resource, which is overkill I think, but I guess it is there for a reason.
readonly product = computed(() => this.productResource.value());
Finally, we can use the methods error
and isLoading
to either show a error
or loading message, if something went wrong, or the data is still loading.
We finally use the computed
to show the value.
@if(productResource.error()) {
Some error occourred.
} @else if(productResource.isLoading()) {
Loading...
} @else {
{{product() | json}}
}
import { Component, inject, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { bootstrapApplication } from '@angular/platform-browser';
import {
ActivatedRoute,
provideRouter,
RouterLink,
RouterOutlet,
Params,
} from '@angular/router';
import { provideHttpClient, httpResource } from '@angular/common/http';
import { JsonPipe } from '@angular/common';
import { map } from 'rxjs';
@Component({
selector: 'app-child',
imports: [JsonPipe],
template: `
@if(productResource.error()) {
Some error occourred.
} @else if(productResource.isLoading()) {
Loading...
} @else {
{{product() | json}}
}
`,
})
export class Child {
private _route = inject(ActivatedRoute);
readonly productId = toSignal<number>(
this._route.params.pipe(map((res: Params) => +res['productId']))
);
readonly productResource = httpResource<any>(
() => `https://jsonplaceholder.typicode.com/todos/${this.productId()}`
);
readonly product = computed(() => this.productResource.value());
}
@Component({
selector: 'app-root',
imports: [RouterOutlet, RouterLink],
template: `
<a routerLink="/child/1">
Child 1
</a> |
<a routerLink="/child/2">
Child 1
</a> |
<a routerLink="/child/3">
Child 1
</a>
<div>
<router-outlet/>
</div>
`,
})
export class App {
name = 'Angular';
}
bootstrapApplication(App, {
providers: [
provideRouter([{ path: 'child/:productId', component: Child }]),
provideHttpClient(),
],
});