|
| 1 | +--- |
| 2 | +title: 'Reactive Angular: Loading Data with the Resource API' |
| 3 | +author: Ferdinand Malcher |
| 4 | +mail: ferdinand@malcher.media |
| 5 | +published: 2025-05-13 |
| 6 | +lastModified: 2025-05-13 |
| 7 | +keywords: |
| 8 | + - Resource API |
| 9 | + - Promise |
| 10 | + - Observable |
| 11 | + - resource |
| 12 | + - rxResource |
| 13 | + - Fetch API |
| 14 | +language: en |
| 15 | +header: header-resource-api.jpg |
| 16 | +--- |
| 17 | + |
| 18 | +An interesting new feature in Angular is the *Resource API*. It allows us to intuitively load data and process it in components. |
| 19 | +In this blog post, we introduce the ideas behind this new interface. |
| 20 | + |
| 21 | +A Resource represents a data set that is loaded asynchronously. This usually involves HTTP requests to fetch data from a server. However, Resource goes a step further than just executing a simple HTTP request: The data can be reloaded at any time or even manually overwritten. Additionally, Resource independently provides information about the loading state. All information and data are exposed as signals, ensuring the current value is always available when changes occur. |
| 22 | + |
| 23 | + |
| 24 | +> 🇩🇪 This article is available in German language here: [Neu in Angular 19: Daten laden mit der Resource API](https://angular-buch.com/blog/2024-10-resource-api) |
| 25 | +
|
| 26 | + |
| 27 | +## What happened before: Example without Resource |
| 28 | + |
| 29 | +To start, let's consider a scenario implemented in the classic way, without the new Resource API. |
| 30 | + |
| 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. |
| 33 | + |
| 34 | +In the component, we need a `books` property to cache the data for display in the template. |
| 35 | +The property is initialized as a signal, following modern practices. |
| 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 | + |
| 38 | +```ts |
| 39 | +@Component({ /* ... */ }) |
| 40 | +export class BookListComponent { |
| 41 | + private bs = inject(BookStoreService); |
| 42 | + books = signal<Book[]>([]); |
| 43 | + |
| 44 | + constructor() { |
| 45 | + this.bs.getAll().subscribe(receivedBooks => { |
| 46 | + this.books.set(receivedBooks); |
| 47 | + }); |
| 48 | + } |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +However, it usually doesn't stop with this simple scenario as additional requirements arise: |
| 53 | + |
| 54 | +- **The book list should be reloadable on button click.** This requires creating a new method (e.g., `reloadList()`) to restart the HTTP request, subscribe again, etc. This duplicates the constructor code. |
| 55 | +- **No parallel requests should be made.** If data is to be reloaded while a previous request is still running, it should either be canceled or the new request ignored. |
| 56 | +- **A loading indicator should be shown.** We could add a `loading` property and toggle it to `true` or `false` at the appropriate points. |
| 57 | +- **Data should be modifiable/overwritable locally.** We could set a new value on the signal. But afterwards, we wouldn't know whether the value was set locally or loaded from the server. |
| 58 | +- **The subscription should end when the component is destroyed.** For this we can use [`takeUntilDestroyed()`](https://angular.dev/api/core/rxjs-interop/takeUntilDestroyed) or another RxJS-based solution. |
| 59 | + |
| 60 | +All these aspects can of course be implemented with moderate effor, but we often need to repeat similar steps to achieve our goal. |
| 61 | +Instead of using imperative style as shown, we could also use RxJS. However, the core issue remains: it's relatively tedious to implement recurring everyday tasks. |
| 62 | + |
| 63 | +The new Resource API aims to fill this gap! |
| 64 | + |
| 65 | +## The new Resource API |
| 66 | + |
| 67 | +A Resource represents data loaded via a loader function. |
| 68 | +We initialize it using the `resource()` function. |
| 69 | +The provided loader is a function that performs the asynchronous data loading. |
| 70 | +This loader runs immediately when the Resource is initialized. |
| 71 | + |
| 72 | +The documentation describes the Resource as follows: |
| 73 | + |
| 74 | +> A Resource is an asynchronous dependency (for example, the results of an API call) that is managed and delivered through signals. |
| 75 | +> [It] projects a reactive request to an asynchronous operation defined by a loader function, which exposes the result of the loading operation via signals. |
| 76 | +
|
| 77 | +```ts |
| 78 | +import { resource } from '@angular/core'; |
| 79 | +// ... |
| 80 | + |
| 81 | +myResource = resource({ |
| 82 | + loader: () => /* load data */ |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +Interestingly, the loader must return a Promise. Though there is nothing wrong with this native browser model, Angular has traditionally used Observables and RxJS for asynchronous operations. |
| 87 | +Angular breaks from tradition here by favoring the browser's native construct. |
| 88 | + |
| 89 | +To perform an HTTP request using a Resource, we have three options: |
| 90 | + |
| 91 | +- 1.) Use an HTTP client that returns Promises, such as the native `fetch()` or the `axios` library. |
| 92 | +- 2.) Use the `firstValueFrom()` function from RxJS to convert an Observable into a Promise that resolves with the first item. |
| 93 | +- 3.) Use an `rxResource`, which uses an Observable as the loader. More on that later! |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +### Option 1: Promises and the native Fetch API |
| 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. |
| 100 | + |
| 101 | +```ts |
| 102 | +@Injectable({ /* ... */ }) |
| 103 | +export class BookStoreService { |
| 104 | + // ... |
| 105 | + getAll(): Promise<Book[]> { |
| 106 | + return fetch(this.apiUrl + '/books').then(res => res.json()); |
| 107 | + } |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +```ts |
| 112 | +// Component |
| 113 | +booksResource = resource({ |
| 114 | + loader: () => this.bs.getAll() |
| 115 | +}); |
| 116 | +``` |
| 117 | + |
| 118 | +### Option 2: Observables and Angular's `HttpClient` |
| 119 | + |
| 120 | +We use Angular's `HttpClient` as usual, so the `getAll()` method returns an Observable. |
| 121 | +To define the loader, we must convert the Observable to a Promise using `firstValueFrom()`. |
| 122 | + |
| 123 | +```ts |
| 124 | +@Injectable({ /* ... */ }) |
| 125 | +export class BookStoreService { |
| 126 | + // ... |
| 127 | + getAll(): Observable<Book[]> { |
| 128 | + return this.http.get<Book[]>(this.apiUrl + '/books'); |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +```ts |
| 134 | +// Component |
| 135 | +booksResource = resource({ |
| 136 | + loader: () => firstValueFrom(this.bs.getAll()) |
| 137 | +}); |
| 138 | +``` |
| 139 | + |
| 140 | +## Accessing the data |
| 141 | + |
| 142 | +The loader is executed immediately when the Resource object is initialized. The Resource processes the response and offers the following signals to work with the data: |
| 143 | + |
| 144 | +- `value`: loaded data, here `Book[]` |
| 145 | +- `status`: state of the Resource, type `ResourceStatus`, e.g., *Resolved* or *Loading*, see next section |
| 146 | +- `error`: error |
| 147 | + |
| 148 | +We can display the loaded books in the template like this: |
| 149 | + |
| 150 | +```html |
| 151 | +{{ booksResource.value() | json }} |
| 152 | + |
| 153 | +@for(book of booksResource.value(); track book.isbn) { |
| 154 | + <p>{{ book.title }}</p> |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +## Status of the Resource |
| 159 | + |
| 160 | +Using the `status` signal, we can evaluate the state of the Resource, e.g., to show a loading indicator. All `status` values are fields from the [`ResourceStatus` enum](https://angular.dev/api/core/ResourceStatus): |
| 161 | + |
| 162 | +| Status from `ResourceStatus` | Description | |
| 163 | +| ---------------------------- | ----------------------------------------------------------------------- | |
| 164 | +| `Idle` | No request is defined and nothing is loading. `value()` is `undefined`. | |
| 165 | +| `Error` | Loading failed. `value()` is `undefined`. | |
| 166 | +| `Loading` | The Resource is currently loading. | |
| 167 | +| `Reloading` | The Resource is reloading after `reload()` was called. | |
| 168 | +| `Resolved` | Loading is complete. | |
| 169 | +| `Local` | The value was overwritten locally. | |
| 170 | + |
| 171 | + |
| 172 | +For a loading indicator, we could process the state in a computed signal and return a boolean if the Resource is currently loading: |
| 173 | + |
| 174 | +```ts |
| 175 | +import { resource, computed, ResourceStatus } from '@angular/core'; |
| 176 | +// ... |
| 177 | + |
| 178 | +isLoading = computed(() => this.booksResource.status() === ResourceStatus.Loading); |
| 179 | +``` |
| 180 | + |
| 181 | +```html |
| 182 | +@if (isLoading()) { |
| 183 | + <div>LOADING</div> |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +To cover all cases, we also need to account for the `Reloading` state. |
| 188 | +Using the built-in `isLoading` property solves this quickly: this signal returns `true` if the Resource is in the `Loading` or `Reloading` state: |
| 189 | + |
| 190 | +```html |
| 191 | +@if (booksResource.isLoading()) { |
| 192 | + <div>LOADING</div> |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | + |
| 197 | + |
| 198 | +## Reloading the Resource |
| 199 | + |
| 200 | +A Resource provides a `reload()` method. |
| 201 | +When called, the loader function is executed again internally and the data is reloaded. |
| 202 | +The result is then again available through the `value` signal. |
| 203 | + |
| 204 | +```html |
| 205 | +<button (click)="reloadList()">Reload book list</button> |
| 206 | +``` |
| 207 | + |
| 208 | +```ts |
| 209 | +@Component({ /* ... */ }) |
| 210 | +export class BookListComponent { |
| 211 | + booksResource = resource({ /* ... */ }); |
| 212 | + |
| 213 | + reloadList() { |
| 214 | + this.booksResource.reload(); |
| 215 | + } |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +The Resource ensures that only one request is executed at a time. |
| 220 | +Reloading is only possible once the previous load has completed. |
| 221 | +You can see this behavior clearly in the [Angular source code](https://github.com/angular/angular/blob/20.0.0/packages/core/src/resource/resource.ts#L294-L296). |
| 222 | + |
| 223 | + |
| 224 | +## Overwriting the Value Locally |
| 225 | + |
| 226 | +The Resource allows the value to be overwritten locally. |
| 227 | +The `value` signal is a `WritableSignal` and offers the familiar `set()` and `update()` methods. |
| 228 | + |
| 229 | +We want to sort the book list locally on button click, sorted by rating. |
| 230 | +In the method, we can sort the list and directly overwrite the `value` signal. |
| 231 | + |
| 232 | + |
| 233 | +```ts |
| 234 | +@Component({ /* ... */ }) |
| 235 | +export class BookListComponent { |
| 236 | + booksResource = resource({ /* ... */ }); |
| 237 | + |
| 238 | + sortBookListLocally() { |
| 239 | + const currentBookList = this.booksResource.value(); |
| 240 | + |
| 241 | + if (currentBookList) { |
| 242 | + const sortedList = currentBookList.toSorted((a, b) => b.rating - a.rating); |
| 243 | + this.booksResource.value.set(sortedList); |
| 244 | + } |
| 245 | + } |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +We want to point out two things in this code: |
| 250 | + |
| 251 | +- The `value` signal returns type `T | undefined`, in our case `Book[] | undefined`. If the data hasn't been loaded yet, the value is `undefined`. Therefore, we need to check whether `currentBookList` exists. We can also pass a default value through the `defaultValue` option to avoid this behavior. |
| 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 | + |
| 254 | + |
| 255 | +## `request`: Loader with Parameter |
| 256 | + |
| 257 | +Our app should have a detail page that displays a single book. |
| 258 | +So the HTTP request must receive information about which book to load. |
| 259 | +When navigating to a different detail page, loading must restart for another book. |
| 260 | + |
| 261 | +The loader must therefore be able to work with parameters. |
| 262 | +Let's assume the component has an input property `isbn` through which the current ISBN is available. |
| 263 | + |
| 264 | +In the loader, we could now use the signal `this.isbn` to pass the ISBN to the service: |
| 265 | + |
| 266 | +```ts |
| 267 | +@Component({ /* ... */ }) |
| 268 | +export class BookDetailsComponent { |
| 269 | + isbn = input.required<string>(); |
| 270 | + |
| 271 | + bookResource = resource({ |
| 272 | + // NOTE: Only executed once! |
| 273 | + loader: () => this.bs.getSingle(this.isbn()) |
| 274 | + }); |
| 275 | +} |
| 276 | +``` |
| 277 | + |
| 278 | +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 | +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 | + |
| 282 | +The request signal thus provides the parameters with which the loader is executed. |
| 283 | + |
| 284 | +```ts |
| 285 | +@Component({ /* ... */ }) |
| 286 | +export class BookDetailsComponent { |
| 287 | + isbn = input.required<string>(); |
| 288 | + |
| 289 | + bookResource = resource({ |
| 290 | + request: this.isbn, |
| 291 | + loader: () => this.bs.getSingle(this.isbn()) |
| 292 | + }); |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +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. |
| 298 | +This allows us to outsource the loader to a separate function and reuse it in other Resources. |
| 299 | + |
| 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. |
| 301 | + |
| 302 | +```ts |
| 303 | +@Component({ /* ... */ }) |
| 304 | +export class BookDetailsComponent { |
| 305 | + isbn = input.required<string>(); |
| 306 | + |
| 307 | + bookResource = resource({ |
| 308 | + request: this.isbn, |
| 309 | + loader: ({ request }) => this.bs.getSingle(request) |
| 310 | + }); |
| 311 | +} |
| 312 | +``` |
| 313 | + |
| 314 | +> **Route Parameters with Component Input Binding:** To automatically bind the `isbn` input property to the current route parameter, you can use the router's [*Component Input Binding*](https://netbasal.com/binding-router-information-to-routed-component-inputs-in-angular-78ee92f63e64) feature. |
| 315 | +
|
| 316 | + |
| 317 | +## `rxResource`: Resource with Observables |
| 318 | + |
| 319 | +In all previous examples, we implemented the loader function using Promises. The browser's Fetch API returns a Promise, and the RxJS function `firstValueFrom()` helped us create a Promise from the Observable returned by Angular's `HttpClient`. |
| 320 | + |
| 321 | +Even though Angular now uses signals in many places instead of Observables, reactive programming with RxJS still has its valid use cases. |
| 322 | +Angular therefore provides the `rxResource` function. It works just like `resource`, but the loader function returns an Observable instead. |
| 323 | +This way, we can use Observables from `HttpClient` directly. |
| 324 | + |
| 325 | +```ts |
| 326 | +@Injectable({ /* ... */ }) |
| 327 | +export class BookStoreService { |
| 328 | + // ... |
| 329 | + getAll(): Observable<Book[]> { |
| 330 | + return this.http.get<Book[]>(this.apiUrl + '/books'); |
| 331 | + } |
| 332 | +} |
| 333 | +``` |
| 334 | + |
| 335 | +```ts |
| 336 | +import { rxResource } from '@angular/core/rxjs-interop'; |
| 337 | +// ... |
| 338 | + |
| 339 | +booksResource = rxResource({ |
| 340 | + loader: () => this.bs.getAll() |
| 341 | +}); |
| 342 | +``` |
| 343 | + |
| 344 | +## Cancelling Ongoing Requests |
| 345 | + |
| 346 | +The Resource provides a way to cancel a running request when a new one is started. |
| 347 | +Especially for loaders with parameters (like the ISBN on the detail page), it's important that only the most recently requested data is processed. |
| 348 | + |
| 349 | +The `rxResource` manages this mechanism internally, because an Observable provides a direct way to cancel the request. |
| 350 | + |
| 351 | +For loaders based on Promises, cancelling is a bit more complicated. |
| 352 | +The loader also receives a so-called `AbortSignal` in its parameter object. |
| 353 | +This is a native browser object that indicates when the request should be aborted. |
| 354 | + |
| 355 | +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. |
| 357 | + |
| 358 | +```ts |
| 359 | +@Component({ /* ... */ }) |
| 360 | +export class BookDetailsComponent { |
| 361 | + isbn = input.required<string>(); |
| 362 | + |
| 363 | + bookResource = resource({ |
| 364 | + request: this.isbn, |
| 365 | + loader: ({ abortSignal }) => fetch( |
| 366 | + detailsUrl + '/' + this.isbn(), |
| 367 | + { signal: abortSignal } |
| 368 | + ) |
| 369 | + }); |
| 370 | +} |
| 371 | +``` |
| 372 | + |
| 373 | +If we're using Angular's `HttpClient` and `firstValueFrom`, cancellation becomes very cumbersome – we would need to convert the `AbortSignal` into an Observable to use the `takeUntil` operator to stop the stream. In this case, we strongly recommend using `rxResource`. |
| 374 | + |
| 375 | +By the way, the Resource also ensures that an active request is stopped when the component is destroyed. |
| 376 | + |
| 377 | +## Conclusion |
| 378 | + |
| 379 | +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. |
| 381 | +Until now, that required a lot of manual effort. |
| 382 | + |
| 383 | +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. |
| 384 | + |
| 385 | +Angular continues its journey toward embracing signals in the framework. The need to use RxJS and Observables for simple tasks is further reduced. |
| 386 | + |
| 387 | +It remains to be seen what role Angular's `HttpClient` will play in the future. By promoting the use of Promises, Angular encourages HTTP communication via the native Fetch API. It would be desirable for `HttpClient` and Resource to work seamlessly together. One could imagine `HttpClient` directly returning a Resource, avoiding the intermediate step through an Observable or Promise. |
| 388 | +In our view, the new interface is a solid foundation-and we're excited to see what comes next! |
| 389 | + |
| 390 | + |
| 391 | +<hr> |
| 392 | +<small>Many thanks to Johannes Hoppe and Danny Koppenhagen for review and feedback.</small> |
| 393 | + |
| 394 | +<small>**Cover image:** Photo by <a href="https://unsplash.com/de/@thepaintedsquarejessica">Jessica Lewis 🦋 thepaintedsquare</a> on <a href="https://unsplash.com/de/fotos/geschnittene-erdbeeren-auf-blaugrunem-keramikteller-15nvaBz_doc">Unsplash</a> (edited) </small> |
0 commit comments