You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -29,20 +29,20 @@ A Resource represents a data set that is loaded asynchronously. This usually inv
29
29
To start, let's consider a scenario implemented in the classic way, without the new Resource API.
30
30
31
31
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.
33
33
34
34
In the component, we need a `books` property to cache the data for display in the template.
35
35
The property is initialized as a signal, following modern practices.
36
36
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.
37
37
38
38
```ts
39
39
@Component({ /* ... */ })
40
-
exportclassBookListComponent {
41
-
privatebs =inject(BookStoreService);
40
+
exportclassBookList {
41
+
#bs =inject(BookStore);
42
42
books =signal<Book[]>([]);
43
43
44
44
constructor() {
45
-
this.bs.getAll().subscribe(receivedBooks=> {
45
+
this.#bs.getAll().subscribe(receivedBooks=> {
46
46
this.books.set(receivedBooks);
47
47
});
48
48
}
@@ -96,11 +96,11 @@ To perform an HTTP request using a Resource, we have three options:
96
96
97
97
### Option 1: Promises and the native Fetch API
98
98
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.
|`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`. |
165
165
|`error`| Loading failed. `value()` is `undefined`. |
166
166
|`loading`| The Resource is currently loading. |
167
167
|`reloading`| The Resource is reloading after `reload()` was called. |
@@ -207,7 +207,7 @@ The result is then again available through the `value` signal.
207
207
208
208
```ts
209
209
@Component({ /* ... */ })
210
-
exportclassBookListComponent {
210
+
exportclassBookList {
211
211
booksResource =resource({ /* ... */ });
212
212
213
213
reloadList() {
@@ -232,7 +232,7 @@ In the method, we can sort the list and directly overwrite the `value` signal.
232
232
233
233
```ts
234
234
@Component({ /* ... */ })
235
-
exportclassBookListComponent {
235
+
exportclassBookList {
236
236
booksResource =resource({ /* ... */ });
237
237
238
238
sortBookListLocally() {
@@ -252,7 +252,7 @@ We want to point out two things in this code:
252
252
- 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.
253
253
254
254
255
-
## `request`: Loader with Parameter
255
+
## `params`: Loader with Parameter
256
256
257
257
Our app should have a detail page that displays a single book.
258
258
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
265
265
266
266
```ts
267
267
@Component({ /* ... */ })
268
-
exportclassBookDetailsComponent {
269
-
isbn =input.required<string>();
268
+
exportclassBookDetails {
269
+
#bs =inject(BookStore);
270
+
readonly isbn =input.required<string>();
270
271
271
272
bookResource =resource({
272
273
// NOTE: Only executed once!
273
-
loader: () =>this.bs.getSingle(this.isbn())
274
+
loader: () =>this.#bs.getSingle(this.isbn())
274
275
});
275
276
}
276
277
```
277
278
278
279
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()`).
279
280
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.
281
282
282
283
The request signal thus provides the parameters with which the loader is executed.
283
284
284
285
```ts
285
286
@Component({ /* ... */ })
286
-
exportclassBookDetailsComponent {
287
-
isbn =input.required<string>();
287
+
exportclassBookDetails {
288
+
#bs =inject(BookStore);
289
+
readonly isbn =input.required<string>();
288
290
289
291
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())
292
296
});
293
297
}
294
298
```
295
299
296
300
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.
298
302
This allows us to outsource the loader to a separate function and reuse it in other Resources.
299
303
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.
@@ -337,7 +344,7 @@ import { rxResource } from '@angular/core/rxjs-interop';
337
344
// ...
338
345
339
346
booksResource=rxResource({
340
-
loader: () =>this.bs.getAll()
347
+
stream: () =>this.#bs.getAll()
341
348
});
342
349
```
343
350
@@ -353,17 +360,18 @@ The loader also receives a so-called `AbortSignal` in its parameter object.
353
360
This is a native browser object that indicates when the request should be aborted.
354
361
355
362
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.
357
364
358
365
```ts
359
366
@Component({ /* ... */ })
360
-
exportclassBookDetailsComponent {
361
-
isbn =input.required<string>();
367
+
exportclassBookDetails {
368
+
#bs =inject(BookStore);
369
+
readonly isbn =input.required<string>();
362
370
363
371
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,
367
375
{ signal: abortSignal }
368
376
)
369
377
});
@@ -374,10 +382,44 @@ If we're using Angular's `HttpClient` and `firstValueFrom`, cancellation becomes
374
382
375
383
By the way, the Resource also ensures that an active request is stopped when the component is destroyed.
376
384
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
+
377
419
## Conclusion
378
420
379
421
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.
381
423
Until now, that required a lot of manual effort.
382
424
383
425
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