angulartypescriptrxjsangular-httpclientangular-signals

How to do a http request for user details everytime the userID signal value changes?


I'm just getting used to performing http requests with signals in Angular 20.

I'd like a http get request to be executed everytime the profileUserID signal value (which I have defined in a user service) is changed. I've started by converting the observable returned from the http request to a signal with toSignal as follows:

userByID = toSignal(this.http.get<User>(`myapi/user/${this.profileUserID()`}), { initialValue: {} });

This does successfully do the http request with the initial value for the profileUserID signal (which is 1) however I would like it to execute the API call with the new profileUserID when its value changes.

Note: The profileUserID signal value is set in the profile component every time there is a change in the userID parameter within the route.

user service

import { Injectable, inject, signal, linkedSignal } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user';


@Injectable({
  providedIn: 'root',
})
export class UserService {
  private http = inject(HttpClient);

  profileUserID = signal<number>(1);

  constructor() { }
  //userByID should be called everytime profileUserID signal value is changed.
  userByID = toSignal(this.http.get<User>(`myapi/user/${this.profileUserID()`}), { initialValue: {} });

}

profile component

import { Component, inject, OnInit, OnDestroy, computed, linkedSignal } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ActivatedRoute } from '@angular/router';

export class Profile {
  route: ActivatedRoute = inject(ActivatedRoute);
  userService: UserService = inject(UserService);


  ngOnInit(): void {
    this.routeParamsSub = this.route.params.subscribe(routeParams => {

      const userIDFromRoute = Number(routeParams['userID']);
      //Set the profileUserID signal value from the userService to be the 
      //new userID value from the route.
      this.userService.profileUserID.set(userIDFromRoute);
      //Now that a new value has been set for the profileUserID signal
      //userByID should do an API call to get the new user details
      
      //the following always prints the user details for 
      //profileUserID initial value of 1
      console.log(this.userService.userByID());

    });
  }

}

Solution

  • Since you are using Angular 20, You should really consider Resource API for this scenario, it is has error handling, reactivity on signal changes, loading and error state also. This greatly reduces the coding effort for API calls.

    Using rxResource:

    Below is an example of using rxResource which accepts an observable, which you can define on the service level.

    @Injectable({
      providedIn: 'root',
    })
    export class UserService {
      private http = inject(HttpClient);
      constructor() { }
    
      //userByID should be called everytime profileUserID signal value is changed.
      public getUserDetails(profileUserID: number) {
        return this.http.get<User>(`myapi/user/${profileUserID}`});
      }
    }
    

    Then we can simplify the params property to be fetched using toSignal.

    export class Profile {
      route: ActivatedRoute = inject(ActivatedRoute);
      userService: UserService = inject(UserService);
      // auto unsubscribe of param
      profileUserID = toSignal(
        this.route.params.pipe(
          map((routeParams) => {
            return Number(routeParams['userID']);
          })
        )
      );
      userDetailsResource = rxResource({
        params: () => this.profileUserID(),
        stream: ({params: profileUserID}) => this.userService.getUserDetails(profileUserID)
      });
    }
    

    Using withComponentInputBinding:

    You can also consider enabling withComponentInputBinding, which will simplify the fetching of the route params, with less code.

    export class Profile {
      route: ActivatedRoute = inject(ActivatedRoute);
      userService: UserService = inject(UserService);
      // using withComponentInputBinding
      profileUserID = input.required<number>({ alias: 'userID' });
      userDetailsResource = rxResource({
        params: () => this.profileUserID(),
        stream: ({params: profileUserID}) => this.userService.getUserDetails(profileUserID)
      });
    }
    

    To use this, add the provider to the main.ts - bootstrapApplication config.

    bootstrapApplication(AppComponent, {
      providers: [provideRouter(appRoutes, withComponentInputBinding())],
    });
    

    Using httpResource:

    If you do not necessarily need the API observable at the service level, you can leverage httpResource to just directly call the API, with the least amount of code.

    export class Profile {
      route: ActivatedRoute = inject(ActivatedRoute);
      userService: UserService = inject(UserService);
      // auto unsubscribe of param
      profileUserID = toSignal(
        this.route.params.pipe(
          map((routeParams) => {
            return Number(routeParams['userID']);
          })
        )
      );
      // or
      // using withComponentInputBinding
      // profileUserID = input.required<number>({ alias: 'userID' });
    
      userDetailsResource = httpResource(() => `myapi/user/${this.profileUserID()}`);
    }
    

    HTML Usage:

    For the HTML side of the implementation, we can use the state signals, isLoading and error to handle loading and error scenarios.

      @if(userDetailsResource.error()) {
        Failed to fetch user details.
      } @else if (userDetailsResource.isLoading()) {
        Loading...
      } @else {
        {{ userDetailsResource.value() }}
      }