Data fetching patterns in Angular

Lukas
medialesson
Published in
11 min readJun 7, 2024

--

Juntao Qiu (邱俊涛) recently published an article about Data Fetching Patterns in Single-Page Applications on Martin Fowler’s website. It is definitely worth reading, yet all examples use React or pure JavaScript. In the following, we will see how to apply all the described patterns to an Angular application. Due to known differences between Angular and React, the results look quite different.

A framework is not a library

React is a library. It lets you put components together, but it doesn’t prescribe how to do routing and data fetching.

React Homepage

While React describes itself as a library, Angular can be considered a web framework. It ships with everything we need to build a single-page application, including tools for routing and data fetching. Although Angular doesn’t really prescribe to use such tools, the idea, of course, is to use them. This all comes with advantages and disadvantages, but this is another story. In the end, both approaches allow us to produce high quality code and to create executable web applications.

A slightly different profile component

The source code described here, embedded in an Angular application, is available on Codeberg.

Juntao Qiu implements a Profile component that gets the ID of a user as parameter. After initial rendering and whenever this ID changes, the component fetches some profile data of the given user from a server. It then displays the received information using another component called UserBrief. Later, the component uses a Friends component to display a list of the user's friends.

Lacking a server, the implementation described here models users, their posts and photo albums, as all this is part of the public available JSONPlaceholder API.

One of the main differences of Angular’s HTTP client compared to other implementations is that each method returns an RxJS Observable. RxJS is a library for reactive programming and the main dependency of Angular besides Node.js and TypeScript. In this way, Angular basically suggests going for reactive programming when an application makes network requests, while not prescribing it.

Of course, we could also use any other HTTP client. Or we could convert the Observables to Promises and just slightly adapt Juntao Qiu’s implementation. But in both cases we would lose several benefits. Angular’s HTTP client, for example, allows us to easily use interceptors. And observables are cancelable, an advantage not to be underestimated. Think of users quickly navigating to different pages of an application. Promises continue network request until they return something, even if the triggering component already got destroyed.

The implementation of data fetching patterns in Angular described here follows a reactive programming approach using the built-in features of Angular.¹

Asynchronous State Handler

Please read Juntao Qiu’s section about the Asynchronous State Handler to understand the general idea behind this pattern.

The idea of the Asynchronous State Handler is to combine asynchronous operations with an explicit loading and error state. While an asynchronous operation is running, the status is loading. It ends either in an error state or with the actual result of the operation. Thinking of a generic operation result T, the Asynchronous State Handler is a function that returns T | 'loading' | 'error'. Using a union type underlines that the result is always exactly one state. Using string literal types for loading and error state is simple but descriptive. They can be replaced by other types if required.

An optional extension of the pattern is a retry functionality. Therefore, the Asynchronous State Handler must provide the possibility to trigger the asynchronous operation again and again.

Implementing Asynchronous State Handler in Angular

We can implement the Asynchronous State Handler in Angular with a custom RxJS operator. In simple terms, the operator accepts any asynchronous operation in the form of an observable as an input parameter. It immediately emits the loading state on subscription. When the asynchronous operation completes, the operator returns the result. If anything goes wrong, it returns the error state.

function toAsynchronousStateHandler<T, R>(
projection: (value: T) => Observable<R>,
) {
return switchMap((value: T) =>
projection(value).pipe(
startWith('loading' as const),
catchError(() => of('error' as const)),
),
);
}

In the profile component, we use the operator whenever the route changes, which contains the user ID.

private readonly httpClient = inject(HttpClient);
private readonly activatedRoute = inject(ActivatedRoute);

protected readonly user$: Observable<UserResponse | 'loading' | 'error'> =
this.activatedRoute.params.pipe(
toAsynchronousStateHandler((params) =>
this.httpClient.get<UserResponse>(
`https://jsonplaceholder.typicode.com/users/${params['id']}`,
),
),
);

In the markup of the component, we can use narrowing to determine the current type of the Asynchronous State Handler, almost like in pure TypeScript. By subscribing to the user observable we initialize the initial fetch, by unsubscribing we can also cancel it. Thanks to Angular’s async pipe, there is no need to manually subscribe and unsubscribe the user observable. It will subscribe after the component was rendered and also cancel ongoing HTTP requests when the component get's destroyed.

@if (user$ | async; as user) {
@if (user === "loading") {
Loading...
} @else if (user === "error") {
Error...
} @else {
<app-user-brief [user]="user" />
}
}

The implementation now only lacks the possibility to re-fetch data. In reactive programming we do this, of course, with an observable. We can combine our existing observable with an additional but optional observable of type void. This observable contains no data, it just triggers an action.
To handle everything in our reusable operator, we now return a function. That way, we gain access to the source observable and can combine it with our trigger observable. This trigger observable may never emit anything, which is why we need to extend it with a start value.
Whenever one of the combined observables emits something, the eventually ongoing asynchronous operation will be canceled, before it gets started again.

export function toAsynchronousStateHandler<T, R>(
projection: (value: T) => Observable<R>,
reloadTrigger = new Observable<void>(),
) {
return function (source: Observable<T>) {
return combineLatest([
source,
reloadTrigger.pipe(startWith(void 0))
]).pipe(
switchMap(([value, _]) =>
projection(value).pipe(
startWith('loading' as const),
catchError(() => of('error' as const)),
),
),
);
};
}

In our profile component, we use a standard Subject, which is an observable that allows us to emit values. We pass it to our operator and can then trigger a refetch with the subject's next() function.

private readonly refetchTrigger$$ = new Subject<void>();

protected readonly user$: Observable<UserResponse | 'loading' | 'error'> =
this.activatedRoute.params.pipe(
toAsynchronousStateHandler(
(params) =>
this.httpClient.get<UserResponse>(
`https://jsonplaceholder.typicode.com/users/${params['id']}`,
),
this.refetchTrigger$$.asObservable(),
),
);

protected onRefetch() {
this.refetchTrigger$$.next();
}

Parallel Data Fetching

Please read Juntao Qiu’s section about Parallel Data Fetch to understand the general idea behind this pattern.

As the name implies, the Parallel Data Fetching pattern deals with fetching data in parallel. We can use it to reduce Request Waterfalls.

Implementing Parallel Data Fetching in Angular

With Angular, and thus RxJS, we get several built-in Join Creation Operators which allow us to combine the emitted values of multiple observables. What comes closest to Promise.all for observables is the forkJoin operator. This operator takes and array or dictionary of observables as input parameters and emits an array or dictionary of values, respectively.

As our Asynchronous State Handler operator works for any observable, we can easily extend our profile component to fetch not just the basic user information but also the user’s posts.

protected readonly dataRequest$ = this.activatedRoute.params.pipe(
toAsynchronousStateHandler(
(params) =>
forkJoin({
user: this.httpClient.get<UserResponse>(
`https://jsonplaceholder.typicode.com/users/${params['id']}`,
),
posts: this.httpClient.get<PostResponse[]>(
`https://jsonplaceholder.typicode.com/users/${params['id']}/posts`,
),
}),
this.refetchTrigger$$.asObservable(),
),
);

In the markup, we still use narrowing to determine the current type (or state) of our asynchronous data request. If it’s neither loading nor in error state, we can safely access the structure that we passed to the forkJoin operator.

@if (dataRequest$ | async; as dataRequest) {
@if (dataRequest === "loading") {
Loading...
} @else if (dataRequest === "error") {
Error...
} @else {
<app-user-brief [user]="dataRequest.user" />
<app-posts [posts]="dataRequest.posts" />
}
}

Fallback Markup

Please read Juntao Qiu’s section about Fallback Markup to understand the general idea behind this pattern.

The idea of the Fallback Markup pattern is to reduce the boilerplate code needed to handle the different states of asynchronous operations like loading or error.

Implementing Fallback Markup in Angular

Let’s recap the markup of our profile component. We use a union type for the state of our asynchronous operation to model it close to reality (one state at a time). With Angular’s control flow and type narrowing, we can conditionally render content based on the current type (or state) of our asynchronous operation. If we change the type of the state in any non-backward compatible way, we’ll receive TypeScript errors accordingly.

@if (dataRequest$ | async; as dataRequest) {
@if (dataRequest === "loading") {
Loading...
} @else if (dataRequest === "error") {
Error...
} @else {
<app-user-brief [user]="dataRequest.user" />
<app-posts [posts]="dataRequest.posts" />
}
}

Now we could implement an approach similar to React’s Suspense component. But we would probably lose the type narrowing. Or, even worse, we would lose any type safety.² This often happens when working with Angular templates.³ Of course, we could also create dedicated components for the loading and error state. Or we could use an interceptor to handle the loading and error state of all network requests globally.

However, with Angular’s control flow, we already get a robust and readable built-in solution. Everything else depends on the respective application and its general strategy for error handling and the loading state. An approach, for example, with many representational components and only a few container components that handle all asynchronous operations, reduces the use cases for this pattern by design.

Code Splitting

Please read Juntao Qiu’s section about Code Splitting to understand the general idea behind this pattern.

Code Splitting addresses the issue of large bundle sizes in web applications by dividing the bundle into smaller chunks that are loaded as needed, rather than all at once. This improves initial load time and performance, especially important for large applications or those with many routes.

Implementing Code Splitting in Angular with Router

Using Angular’s built-in router, we can utilize the browser’s dynamic import expression to lazy load modules only when they are really needed. Where module means either an Angular Module (NgModule), a standalone component or another router configuration.

A shortened router configuration of an application that provides the profile component and another component to display photo albums on different routes can look as follows.

import { Routes } from '@angular/router';

export const routes: Routes = [
{
path: 'profile',
loadComponent: () =>
import('./profile/profile.component').then(
(module) => module.ProfileComponent,
)
},
{
path: 'albums',
loadComponent: () =>
import('./albums/albums.component').then(
(module) => module.AlbumsComponent,
)
}
];

Please note that the import statements on top contain no direct reference to the components. During build, Angular tells us which chunks it has created. The chunk without name contains the shared implementation of the Asynchronous State Handler.

Lazy chunk files    | Names             |  Raw size
chunk-OQXIE7JX.js | profile-component | 6.70 kB |
chunk-FX2UNWRV.js | albums-component | 4.96 kB |
chunk-REKZS4LG.js | - | 500 bytes |

By default, there is no indicator when the Angular router is performing a lazy load of a chunk. To increase the user experience, it might be useful to display some loading indicator on the root level of an application. Angular’s router provides two specific events whenever a lazy load starts or end.

export class AppComponent {
private readonly router = inject(Router);

protected readonly isRouterLazyLoading$ = this.router.events.pipe(
filter(
(event) =>
event instanceof RouteConfigLoadStart ||
event instanceof RouteConfigLoadEnd,
),
map((event) => {
if (event instanceof RouteConfigLoadStart) {
return true;
}
return false;
}),
startWith(false),
);
}

Implementing Code Splitting in Angular with Deferrable Views

With Angular’s recently introduced Deferrable Views, we can lazy load components within a component’s template. Such Deferrable Views are similar to React’s Suspense component combined with lazy component loading, but differ in the number of additional features.⁴

Let’s say we don’t want to display all posts of a user in our profile component immediately. Instead, users should click on a dedicated button to display the posts. To achieve this, we can put the component that displays the posts into a Deferrable View and show a button as placeholder. By telling the Deferrable View to load on interaction, a click on the button loads the actual content. We can also pass a dedicated template for the loading and error state, similar to the Asynchronous State Handler.

@defer (on interaction) {
<app-posts [posts]="dataRequest.posts" />
} @placeholder {
<button>Show Posts</button>
} @loading {
Loading posts...
} @error {
Error...
}

What happens behind the scene is that Angular puts the content of the Deferrable View into a separate chunk. To make this work, the content must not be directly referenced anywhere else. We can see the additional chunk in the output of the build.

Lazy chunk files    | Names             |  Raw size
chunk-OQXIE7JX.js | profile-component | 6.70 kB |
chunk-FX2UNWRV.js | albums-component | 4.96 kB |
chunk-4UTUEDCN.js | posts-component | 2.02 kB |
chunk-REKZS4LG.js | - | 500 bytes |

In the network panel of our browser’s developer tools we can see that the new chunk get’s loaded when someone clicks on the button for the first time.

Implementing Code Splitting in Angular with Image Optimization

If we consider images as chunks and Code Splitting addresses “chunks that are loaded as needed, rather than all at once”, it can be worth to treat images the same way as other chunks. Imagine a component that displays dozens of images, many of which are not even visible in the viewport. Depending on the browser, the <img> element will make the browser load all images at once.⁵ Even worse, when the loading of an image is triggered, we can only cancel it by closing the website. When users just navigate to another route within a single-page application, the download of all images continues till it's done.

The albums component of our sample application should display all photo albums of a user by displaying every thumbnail of every photo.

@for (album of albums; track album.id) {
@for (photo of album.photos; track photo.thumbnailUrl) {
<img [src]="photo.thumbnailUrl" />
}
}

Such a naive approach entails all the problems mentioned above. The Image Optimization integrated in Angular is a cross-browser solution that lazy loads images that are not close to the viewport. Besides enforcing a number of image best practices it also supports placeholders.

@for (album of albums; track album.id) {
@for (photo of album.photos; track photo.thumbnailUrl; let index = $index) {
<img
[ngSrc]="photo.thumbnailUrl"
width="150"
height="150"
[priority]="index < 10 // 👈 Prioritize the first 10 images"
[placeholder]="placeholderImage"
/>
}
}

In the network panel of the browser’s developer tools we can track how more and more images get loaded when scrolling down the page. Compare this to the naive approach where hundred of images get loaded in a non-cancelable manner.

Prefetching

Please read Juntao Qiu’s section about Prefetching to understand the general idea behind this pattern.

Prefetching involves loading resources or data ahead of their actual need, aiming to decrease wait times during subsequent operations.

Implementing Prefetching in Angular with Deferrable Views

Angular’s Deferrable Views support to preload the deferred view based on either a custom condition or a predefined trigger. We can, for example, preload the content when the browser is idle or when the user hovers over the placeholder.

@defer (on interaction; prefetch on hover) {
<app-posts [posts]="dataRequest.posts" />
} @placeholder {
<button>Show Posts</button>
} @loading {
Loading posts...
} @error {
Error...
}

Conclusion

Regardless of what tooling we chose, the patterns described by Juntao Qiu are worth to keep in mind whenever we develop web applications. Angular ships with various built-in features, some of them introduced just recently, that support us in applying these patterns in a simplified and standardized way.

Originally published at https://lukket.me on June 6, 2024.

  1. The reactivity of the implementation described here is backed up by the exclusive use of read-only properties.
  2. Yes, talking about any type safety is an intentional pun.
  3. The context of an Angular template was an untyped object up to Angular 16. It’s now a generic type with default unknown. The documentation is unclear about how to make use of the generic type in the markup.
  4. Angular’s Deferrable Views support dedicated templates for a placeholder and the error case. The loading of a Deferrable View can be triggered in various ways and also prefetching is a built-in feature. Nevertheless, a Deferrable View does not intercept a component’s fetch requests (XHR), but React’s Suspense component does.
  5. Firefox does not support the fetchPriority property of the <img> element yet.

--

--