1

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.

3 Answers 3

0

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}}
}

Full Code:

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(),
  ],
});

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you sir you're awesome like your solution. Finally I have completed the task. Thank you again.
0

Solution

  1. Get the ID from the URL
    Use Angular’s ActivatedRoute to extract the ID (e.g., from /items/:id):

    import { signal } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    
    export class YourComponent {
      // Create a Signal for the ID
      id = signal<string>('');
    
      constructor(private route: ActivatedRoute) {
        // Bind URL ID to the Signal
        this.route.params.subscribe(params => {
          this.id.set(params['id']); // Update Signal when ID changes
        });
      }
    }
    
  2. Fetch Data Using the Signal
    Use computed or effect to react to Signal changes (Angular 16+):

    import { computed, effect } from '@angular/core';
    
    export class YourComponent {
      id = signal<string>('');
      data = signal<any>(null);
    
      constructor(private route: ActivatedRoute, private http: HttpClient) {
        // Fetch data when ID changes
        effect(() => {
          if (this.id()) { // Only fetch if ID exists
            this.http.get(`/api/items/${this.id()}`).subscribe(response => {
              this.data.set(response); // Update data Signal
            });
          }
        }, { allowSignalWrites: true });
      }
    }
    
  3. Template Binding
    Access Signals directly in the template:

    <div *ngIf="data() as item">
      {{ item.name }}
    </div>
    

Key Points

  • Why Signals? They provide reactive updates without RxJS subscriptions.
  • Avoid Memory Leaks: Use takeUntilDestroyed (Angular 16+) if needed.
  • Error Handling: Add .catch() for HTTP failures.

1 Comment

It's an honor for me to have a solution from you. But I have a question that is in your solution you don't apply HttpResource. Actually the task I want to complete using HttpResource.
0

You don't need activatedRoute. You can extract params from url directly to input giving the input the same name. Just add withComponentInputBinding() to app config.

To react to changes you can use effect or toSignal(toObservable(this.productId).pipe(switchMap(() => this.getData())))

import { Component, input, effect } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  provideRouter,
  RouterLink,
  RouterOutlet,
  withComponentInputBinding,
} from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-child',
  template: ``,
})
export class Child {
  readonly productId = input();
  readonly productIdReactive = toSignal(toObservable(this.productId));

  constructor() {
    effect(() => {
      console.log('productIdInput', this.productId());
      console.log('productIdReactive', this.productIdReactive());
    });
  }
}

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <a routerLink="/child/1">
      Child 1
    </a> | 
    <a routerLink="/child/2">
      Child 2
    </a> | 
    <a routerLink="/child/3">
      Child 3
    </a>
    <div>
      <router-outlet/>
    </div>
  `,
})
export class App {
  name = 'Angular';
}

bootstrapApplication(App, {
  providers: [
    provideRouter(
      [{ path: 'child/:productId', component: Child }],
      withComponentInputBinding()
    ),
    provideHttpClient(),
  ],
});

StackBlitz

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.