← All articles

Managing HTTP Requests in the Angular Signals Era

From basic HttpClient plus signals to the resource API, httpResource trade-offs, and a practical infinite-scroll pattern driven by signal state.

Published
Managing HTTP Requests in the Angular Signals Era cover image

Managing HTTP Requests in the Angular Signals Era

Signals, introduced in Angular 16 and later, add a new option for state management in Angular. This article explains how to manage HTTP requests efficiently with Signals, with practical examples.

HTTP request basics

First, here is a basic HTTP request with Signals. If you are not doing side-effectful work, you can also bind directly in the template and manage display with the async pipe. In that case, the request runs when the template renders.

@Component({
  template:
  `@if (!isLoading()) { @for (item of users(); track item.id) { ... } }`,
  ...
})
export class UserComponent implements OnInit {
  private readonly http = inject(HttpClient);
  readonly users = signal<User[]>([]);
  readonly isLoading = signal<boolean>(false);

  ngOnInit() {
    this.isLoading.set(true);
    this.http.get<User[]>('/api/users').subscribe({
      next: (data) => {
        this.isLoading.set(false);
        this.users.set(data);
    });
  }
}

Note that you cannot re-request for data updates with this alone. If you need a refetch, you have to build your own Observable and call next, which takes a bit of work.

@Component({...})
export class ExampleComponent {
  private refreshTrigger = new Subject<void>();

  data$: Observable<any> = this.refreshTrigger.pipe(
    startWith(undefined),
    switchMap(() => this.http.get('/api/users'))
  );
}

If you use the value inside the class, you need to subscribe to data$ and unsubscribe via Angular lifecycle hooks. That is the familiar pattern.

Implementation with the Resource API

The Resource API introduced in Angular 17 lets you manage HTTP requests more declaratively. It still fires when the template renders, but the resource API includes a reload method, so refetching is easy.

@Component({
  template: 
  `@if (!users.isLoading()) { @for (item of users.value(); track item.id) { ... } }`,
  ...
})
export class UserComponent {
  private http = inject(HttpClient);
  users = resource({
    loader: () => firstValueFrom(this.http.get<User[]>('/api/users'))
  })

  // This alone triggers a new request; it can also be done from the template
  reload = () => this.users.reload();
}

You can also use the request property to refetch whenever the Signals passed to request change.

@Component({...})
export class UserComponent {
  private http = inject(HttpClient);
  userId = signal<number>(1);
  users = resource({
    request: () => ({userId: userId()}),
    loader: ({request}) => firstValueFrom(this.http.get<User[]>('/api/users/' + request.userId))
  })
}

Compared with managing everything through Observable, this is much simpler. Because behavior is driven by Signal state rather than lifecycle or user actions, it is easier to write pure logic and improve testability.

What about httpResource?

Above, I convert HttpClient to a Promise and pair it with the resource API. There is also an experimental httpResource API that combines the two.

@Component({...})
export class UserComponent {
  userId = signal<number>(1);
  users = httpResource('/api/users/' + this.userId());
}

Internally it is an HttpRequest, so Interceptor still works, which is great. In a real project, though, you may hit a few pain points.
You may need to transform the response, and if several components share the data, you probably want a Service. For example:

@Service({...})
export class UserService {
  resourceUsers(userId: WritableSignal) {
    return httpResource('/api/users/' + this.userId(), {
        map: (data) => {
        // Perform the necessary complex processing here
        return data;
    }
    });
  }
}

However, resourceUsers is not a Promise, so you cannot resolve it with await this.userService.resourceUsers(this.userId()). If another component needs this value from a click handler, you must preload it as a property first. Alternatively, wrap it in a Promise and poll with setInterval until a value appears, or convert with toObservable and then to a Promise. Neither feels practical.

https://github.com/angular/angular/issues/58917https://github.com/angular/angular/issues/58917

Because it is still experimental, Angular may add a fix later, but for overall project structure I would adopt httpResouce carefully today.

Practical HTTP requests

Fetching data to render a component is practical enough, but let us go one step further.

Infinite scroll implementation

Consider infinite scroll. The Signal that drives request should represent the scroll page—the page number to fetch (or, depending on implementation, the last item id). Use a page WritableSignal for that. When showing page 0, you need requests for:

  • Initial display
  • Refresh

In those cases, you should show only that page's items. When scrolling, you need to append fetched items to what is already shown.

@Component({...})
export class InfiniteScrollComponent {
  private http = inject(HttpClient);
  
  page = signal(0);
  items = signal<Item[]>([]);

  constructor() {
    effect(async () => {
      const page = this.page(); // React to this value
      const data = await firstValueFrom(this.http.get('/api/users/' + page));
      this.items.update(item => {
        if (page === 0) {
            return [...data]
        } else {
            return [...item, ...data]
        }
      })
    })
  }
  
  // Request the next page
  loadMore = () => this.page.update(page => page + 1);
  refresh = () => this.page.set(0);
}

Before, I wrote handlers for user events. With Signals, I write logic against state changes, which stays much simpler.

Summary

Signals make HTTP request state management more intuitive and improve readability. Benefits include:

  • Easier reactive state management
  • Clearer component state
  • Simpler async state handling
  • Easier performance tuning

Signals are a newer Angular feature and should see more use over time. For HTTP requests, Signals help you write more maintainable code.

See you next time.