In an Angular 22 app, when would you fetch the reviews panel's data with httpResource, and when with HttpClient plus toSignal() or rxResource() instead?
answer
- reads keyed by signals
- writes stay imperative
- who needs RxJS operators
- a service already returns Observables
basics
~20 sUse httpResource for signal-keyed GET reads shown in a view. Use HttpClient directly for writes, rxResource when the loader must be an Observable pipeline or an existing service method, and toSignal for one fixed stream that does not refetch when inputs change.
solid answer
~40 s`httpResource` fits the common case: a **read** keyed by signals — `productId` in, reviews out — with status, error and HTTP status as signals and automatic cancellation when the key changes. It is a poor fit for **mutations**: the guide advises `HttpClient` directly for `POST`/`PUT`, because an eager, re-running request suits reading, not writing. When the loader needs **RxJS composition** — retry with backoff, polling, combining calls, several emissions — or when a **service already returns an Observable**, `rxResource({ params, stream })` keeps the resource model but lets the loader be any Observable. `toSignal(http.get(...))` suits a single fixed request with no key: it subscribes once, never refetches on input changes, and exposes no status or cancellation. All of these still go through `HttpClient`, so interceptors apply everywhere.
code
ts · 51 linesimport { Component, Injectable, inject, input } from '@angular/core';
import { HttpClient, httpResource } from '@angular/common/http';
import { rxResource } from '@angular/core/rxjs-interop';
import { Observable, retry, timer } from 'rxjs';
interface Review {
id: string;
rating: number;
text: string;
}
@Injectable({ providedIn: 'root' })
export class ReviewsApi {
private readonly http = inject(HttpClient);
list(productId: string): Observable<Review[]> {
return this.http.get<Review[]>(`/api/products/${productId}/reviews`);
}
add(productId: string, review: Omit<Review, 'id'>): Observable<Review> {
return this.http.post<Review>(`/api/products/${productId}/reviews`, review);
}
}
@Component({
selector: 'app-product-reviews',
template: `
@if (reviews.hasValue()) {
<p>{{ reviews.value().length }} reviews</p>
}
<button (click)="rate(5)">Rate 5 stars</button>
`,
})
export class ProductReviews {
private readonly api = inject(ReviewsApi);
readonly productId = input.required<string>();
// Read keyed by a signal: httpResource.
readonly reviews = httpResource<Review[]>(() => `/api/products/${this.productId()}/reviews`);
// Same read through an existing service plus RxJS retry: rxResource.
readonly reviewsWithRetry = rxResource({
params: () => this.productId(),
stream: ({ params }) => this.api.list(params).pipe(retry({ count: 2, delay: () => timer(500) })),
});
// Write: plain HttpClient, triggered once by the user, then refresh the read.
rate(rating: number): void {
this.api.add(this.productId(), { rating, text: '' }).subscribe(() => this.reviews.reload());
}
}go deeper
Know that httpResource is for reading data keyed by signals and that saving data still uses HttpClient methods such as post().
Compare httpResource, rxResource and toSignal on refetching, cancellation and status signals, and explain why writes stay imperative.
Choose per use case in a real feature, bridge existing Observable services with rxResource, and refresh reads after writes with reload() or local updates.
Set the team's data-fetching conventions across services and components, including how resource-based reads, Observable services and SSR caching coexist during migration.
## Four tools, one HTTP layer In Angular 22 there are four common ways to get server data into a component. All four end up in `HttpClient`, so interceptors, the backend and HTTP testing work the same; they differ in **who triggers the request** and **what shape the result has**. | Tool | Triggered by | Result | Refetches on input change | Status and cancellation | |---|---|---|---|---| | `httpResource(() => req)` | Creation, then tracked signals | Resource of signals | Yes, cancels the pending call | Built in | | `rxResource({ params, stream })` | Creation, then the `params` signals | Resource of signals | Yes, unsubscribes the previous stream | Built in | | `toSignal(http.get(...))` | Creation of the signal | One signal | No | None; errors are rethrown when the signal is read | | `http.post(...).subscribe()` | Your code, explicitly | Observable | Not applicable | Manual | ## When `httpResource` is the right call - The request is a **read** (`GET`) derived from signals: route parameters, inputs, filters. - The view needs **loading, error and empty states** and the HTTP status, for example a 404 message. - Stale responses must never win when the key changes; the resource cancels them. - The app uses **server-side rendering** with the HTTP transfer cache: `httpResource` picks up the server's response when it is created, so the hydrated view starts with data. - Runtime validation belongs at the edge: the `parse` option. The reviews panel is the textbook case: `productId` in, a list of reviews with loading and error states out. ## When to stay with `HttpClient` 1. **Writes.** Posting a new review, editing, deleting. A resource is eager and re-runs when its signals change; a write must happen exactly once, when the user acts. Call `http.post()` from an event handler, then `reviews.reload()` or update the resource locally with `set()`/`update()`. 2. **Data services with Observable APIs** shared across the app. The `HttpClient` Observable is composable and cancellable, and several consumers can shape it differently. 3. **Flows that are not "one key, one response"**: uploads with progress, request chains with conditional steps, fire-and-forget telemetry. ## When `rxResource` wins `rxResource` (from `@angular/core/rxjs-interop`) keeps the resource model but takes a `stream` function returning an **Observable**: - The loader needs **RxJS operators**: `retry` with backoff, polling with `timer`, `forkJoin` of several calls, a WebSocket stream. - The component should reuse an **existing service method** that returns `Observable<Review[]>` instead of rebuilding the URL in the component. - The data source is not HTTP at all. ## When `toSignal` is enough `toSignal()` converts one Observable into a signal and subscribes immediately. It suits a **fixed request** — configuration, the current user — created once. It does not refetch when an input changes (you would need a new signal), exposes no status, and rethrows the Observable's error when the signal is read. For keyed data, a resource is the better tool. ## Decision questions for a review - Is this a read or a write? Writes go to `HttpClient`. - Is it keyed by signals and shown with loading and error states? Prefer `httpResource`. - Does the loader need operators, or an Observable from a service? Use `rxResource`. - Is it one fixed value for the component's lifetime? `toSignal` is enough. ## Pitfalls when mixing them - Wrapping a write in a resource and discovering it re-posts when a signal changes. - Using `toSignal` for keyed data and building manual `switchMap` plumbing that a resource provides. - Duplicating URL building in components when a service already owns the endpoint; bridge with `rxResource` instead. - Forgetting that all of them are HTTP calls: interceptors, auth and error mapping must work for each.
- After posting a review with HttpClient, how do you get the httpResource-backed list to show it?Either call `reviews.reload()` to refetch the list, which moves the resource to `reloading` and keeps the old list visible until the new one arrives, or update it locally with `reviews.update((list) => [...list, created])`, which sets the status to `local` without a request. Reload is simpler and authoritative; the local update is instant but must match what the server would return.
- Why can httpResource show data immediately during hydration of a server-rendered page?When the app uses the HTTP transfer cache, the server stores the responses of requests it made while rendering. An `httpResource` created in the browser looks up that cached response when it is created and starts with it, running `parse` on it too, instead of sending the request again, so the hydrated view has data at once.
httpResource is a standing order at a shop: tell it which product you want and it keeps delivering the current one, cancelling the old order when you change your mind. HttpClient.post() is walking to the counter to place one order, once, when you decide to.
saying these in an interview costs you the question
- httpResource should replace every HttpClient call, including POST and DELETE.
- toSignal(http.get(url)) refetches automatically when a signal used in url changes.
- rxResource and httpResource bypass interceptors, so auth must be added separately.
- An Observable-returning service method cannot be used with the resource APIs.
- httpResource is lazy like HttpClient and waits for the template to read it.