Skip to content

Commit 375e2c2

Browse files
committed
resource: params, makeover
1 parent 40cfd78 commit 375e2c2

File tree

1 file changed

+79
-37
lines changed

1 file changed

+79
-37
lines changed

blog/2025-05-resource-api/README.md

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: 'Reactive Angular: Loading Data with the Resource API'
33
author: Ferdinand Malcher
44
55
published: 2025-05-13
6-
lastModified: 2025-05-13
6+
lastModified: 2025-06-18
77
keywords:
88
- Resource API
99
- Promise
@@ -29,20 +29,20 @@ A Resource represents a data set that is loaded asynchronously. This usually inv
2929
To start, let's consider a scenario implemented in the classic way, without the new Resource API.
3030

3131
We want to display a list of books in a component, which will be loaded via HTTP from a server.
32-
The corresponding `BookStoreService` already exists and is injected via dependency injection. The `getAll()` method in the service uses Angular's `HttpClient` and returns an Observable.
32+
The corresponding `BookStore` service already exists and is injected via dependency injection. The `getAll()` method in the service uses Angular's `HttpClient` and returns an Observable.
3333

3434
In the component, we need a `books` property to cache the data for display in the template.
3535
The property is initialized as a signal, following modern practices.
3636
In the constructor, we subscribe to the Observable from `getAll()`. As soon as the list of books arrives from the server, we set the data on the `books` signal.
3737

3838
```ts
3939
@Component({ /* ... */ })
40-
export class BookListComponent {
41-
private bs = inject(BookStoreService);
40+
export class BookList {
41+
#bs = inject(BookStore);
4242
books = signal<Book[]>([]);
4343

4444
constructor() {
45-
this.bs.getAll().subscribe(receivedBooks => {
45+
this.#bs.getAll().subscribe(receivedBooks => {
4646
this.books.set(receivedBooks);
4747
});
4848
}
@@ -96,11 +96,11 @@ To perform an HTTP request using a Resource, we have three options:
9696

9797
### Option 1: Promises and the native Fetch API
9898

99-
In the `BookStoreService`, we use the native Fetch API so that the `getAll()` method returns a Promise. In the loader, we can use this Promise directly.
99+
In the `BookStore`, we use the native Fetch API so that the `getAll()` method returns a Promise. In the loader, we can use this Promise directly.
100100

101101
```ts
102102
@Injectable({ /* ... */ })
103-
export class BookStoreService {
103+
export class BookStore {
104104
// ...
105105
getAll(): Promise<Book[]> {
106106
return fetch(this.apiUrl + '/books').then(res => res.json());
@@ -111,7 +111,7 @@ export class BookStoreService {
111111
```ts
112112
// Component
113113
booksResource = resource({
114-
loader: () => this.bs.getAll()
114+
loader: () => this.#bs.getAll()
115115
});
116116
```
117117

@@ -122,7 +122,7 @@ To define the loader, we must convert the Observable to a Promise using `firstVa
122122

123123
```ts
124124
@Injectable({ /* ... */ })
125-
export class BookStoreService {
125+
export class BookStore {
126126
// ...
127127
getAll(): Observable<Book[]> {
128128
return this.http.get<Book[]>(this.apiUrl + '/books');
@@ -133,7 +133,7 @@ export class BookStoreService {
133133
```ts
134134
// Component
135135
booksResource = resource({
136-
loader: () => firstValueFrom(this.bs.getAll())
136+
loader: () => firstValueFrom(this.#bs.getAll())
137137
});
138138
```
139139

@@ -161,7 +161,7 @@ Using the `status` signal, we can evaluate the state of the Resource, e.g., to s
161161

162162
| Status from `ResourceStatus` | Description |
163163
| ---------------------------- | ----------------------------------------------------------------------- |
164-
| `idle` | No request is defined and nothing is loading. `value()` is `undefined`. |
164+
| `idle` | No params are defined and nothing is loading. `value()` is `undefined`. |
165165
| `error` | Loading failed. `value()` is `undefined`. |
166166
| `loading` | The Resource is currently loading. |
167167
| `reloading` | The Resource is reloading after `reload()` was called. |
@@ -207,7 +207,7 @@ The result is then again available through the `value` signal.
207207

208208
```ts
209209
@Component({ /* ... */ })
210-
export class BookListComponent {
210+
export class BookList {
211211
booksResource = resource({ /* ... */ });
212212

213213
reloadList() {
@@ -232,7 +232,7 @@ In the method, we can sort the list and directly overwrite the `value` signal.
232232

233233
```ts
234234
@Component({ /* ... */ })
235-
export class BookListComponent {
235+
export class BookList {
236236
booksResource = resource({ /* ... */ });
237237

238238
sortBookListLocally() {
@@ -252,7 +252,7 @@ We want to point out two things in this code:
252252
- Instead of `Array.sort()`, we use the new method `Array.toSorted()`, which does not mutate the array and returns a sorted copy. This preserves immutability. `toSorted()` can only be used if the `lib` option in `tsconfig.json` includes at least `ES2023`, which is not the case in new Angular projects yet.
253253

254254

255-
## `request`: Loader with Parameter
255+
## `params`: Loader with Parameter
256256

257257
Our app should have a detail page that displays a single book.
258258
So the HTTP request must receive information about which book to load.
@@ -265,48 +265,53 @@ In the loader, we could now use the signal `this.isbn` to pass the ISBN to the s
265265

266266
```ts
267267
@Component({ /* ... */ })
268-
export class BookDetailsComponent {
269-
isbn = input.required<string>();
268+
export class BookDetails {
269+
#bs = inject(BookStore);
270+
readonly isbn = input.required<string>();
270271

271272
bookResource = resource({
272273
// NOTE: Only executed once!
273-
loader: () => this.bs.getSingle(this.isbn())
274+
loader: () => this.#bs.getSingle(this.isbn())
274275
});
275276
}
276277
```
277278

278279
This code basically works – but only once! The loader function is *untracked*. This means it won't automatically rerun when the signal values it depends on change (unlike with `effect()` or `computed()`).
279280

280-
To solve this, we can use the `request` property: Here we pass a signal. Whenever this signal changes its value, the loader will automatically run again.
281+
To solve this, we can use the `params` property: Here we pass a signal or a function that uses signals inside. Whenever these signal change their value, the loader will automatically run again.
281282

282283
The request signal thus provides the parameters with which the loader is executed.
283284

284285
```ts
285286
@Component({ /* ... */ })
286-
export class BookDetailsComponent {
287-
isbn = input.required<string>();
287+
export class BookDetails {
288+
#bs = inject(BookStore);
289+
readonly isbn = input.required<string>();
288290

289291
bookResource = resource({
290-
request: this.isbn,
291-
loader: () => this.bs.getSingle(this.isbn())
292+
params: this.isbn,
293+
// or
294+
params: () => this.isbn(),
295+
loader: () => this.#bs.getSingle(this.isbn())
292296
});
293297
}
294298
```
295299

296300
To make the loader a bit more generic and reusable, we can avoid directly calling `this.isbn()`.
297-
The value from `request` is conveniently passed as an argument to the loader function.
301+
The value from `params` is conveniently passed as an argument to the loader function.
298302
This allows us to outsource the loader to a separate function and reuse it in other Resources.
299303

300-
The loader automatically receives an argument of type `ResourceLoaderParams`, which has a `request` property. In our example, it holds the ISBN returned by the `request` signal.
304+
The loader automatically receives an argument of type `ResourceLoaderParams`, which has a `params` property. In our example, it holds the ISBN returned by the `params` function.
301305

302306
```ts
303307
@Component({ /* ... */ })
304-
export class BookDetailsComponent {
305-
isbn = input.required<string>();
308+
export class BookDetails {
309+
#bs = inject(BookStore);
310+
readonly isbn = input.required<string>();
306311

307312
bookResource = resource({
308-
request: this.isbn,
309-
loader: ({ request }) => this.bs.getSingle(request)
313+
params: this.isbn,
314+
loader: ({ params }) => this.#bs.getSingle(params)
310315
});
311316
}
312317
```
@@ -322,9 +327,11 @@ Even though Angular now uses signals in many places instead of Observables, reac
322327
Angular therefore provides the `rxResource` function. It works just like `resource`, but the loader function returns an Observable instead.
323328
This way, we can use Observables from `HttpClient` directly.
324329

330+
Since an Observable *can* emit an infinite number of values, the property here is called `stream` instead of `loader`.
331+
325332
```ts
326333
@Injectable({ /* ... */ })
327-
export class BookStoreService {
334+
export class BookStore {
328335
// ...
329336
getAll(): Observable<Book[]> {
330337
return this.http.get<Book[]>(this.apiUrl + '/books');
@@ -337,7 +344,7 @@ import { rxResource } from '@angular/core/rxjs-interop';
337344
// ...
338345

339346
booksResource = rxResource({
340-
loader: () => this.bs.getAll()
347+
stream: () => this.#bs.getAll()
341348
});
342349
```
343350

@@ -353,17 +360,18 @@ The loader also receives a so-called `AbortSignal` in its parameter object.
353360
This is a native browser object that indicates when the request should be aborted.
354361

355362
Together with the native Fetch API, this object can be used directly.
356-
If `this.isbn` changes while the loader is still running, the current Fetch request will be aborted.
363+
If `this.isbn` changes while the loader is still running, the current fetch request will be aborted.
357364

358365
```ts
359366
@Component({ /* ... */ })
360-
export class BookDetailsComponent {
361-
isbn = input.required<string>();
367+
export class BookDetails {
368+
#bs = inject(BookStore);
369+
readonly isbn = input.required<string>();
362370

363371
bookResource = resource({
364-
request: this.isbn,
365-
loader: ({ abortSignal }) => fetch(
366-
detailsUrl + '/' + this.isbn(),
372+
params: this.isbn,
373+
loader: ({ abortSignal, aprams }) => fetch(
374+
detailsUrl + '/' + params,
367375
{ signal: abortSignal }
368376
)
369377
});
@@ -374,10 +382,44 @@ If we're using Angular's `HttpClient` and `firstValueFrom`, cancellation becomes
374382

375383
By the way, the Resource also ensures that an active request is stopped when the component is destroyed.
376384

385+
386+
## httpResource: Resource for HTTP Requests
387+
388+
In early 2025, another variant of the Resource was introduced: `httpResource`.
389+
It uses Angular's `HttpClient` under the hood to perform an HTTP request directly.
390+
You no longer need to write the request yourself – the resource handles it for you.
391+
392+
```ts
393+
booksResource = httpResource<Book[]>(
394+
() => 'https://api.example.org/books',
395+
{ defaultValue: [] }
396+
);
397+
```
398+
399+
The request must be generated using a function.
400+
This is because it runs in a *reactive context*: If you use signals inside the function, the request is re-executed automatically when any of those signals change. This is similar to the `params` property in a resource.
401+
Additional request details can be passed in an options object:
402+
403+
```ts
404+
booksResource = httpResource<Book[]>(
405+
() => ({
406+
url: 'https://api.example.org/books',
407+
params: {
408+
search: 'Angular'
409+
}
410+
})
411+
);
412+
```
413+
414+
Please note that a resource is only meant for *retrieving* data from an API and exposing it with signals.
415+
Write operations such as create, update, or delete cannot be handled with a resource.
416+
You must continue to use `HttpClient` directly for those.
417+
418+
377419
## Conclusion
378420

379421
With the new Resource API, Angular introduces an intuitive and well-integrated interface for loading data from a server.
380-
Use cases beyond a simple HTTP request-especially reloading data and showing a loading indicator-can be implemented quickly with the Resource.
422+
Use cases beyond a simple HTTP request, especially reloading data and showing a loading indicator, can be implemented quickly with the Resource.
381423
Until now, that required a lot of manual effort.
382424

383425
We welcome Angular's focus on addressing this common everyday problem. The solution covers most use cases reliably and offers a standardized approach-only more advanced needs will require custom implementation going forward.

0 commit comments

Comments
 (0)