When you build apps with Angular, you often need to get data when something changes in your app. These changes could come from a user's action, like typing in a search box, or from the app itself, such as a change in the route parameters. Today, we're going to learn some effective ways to reactively fetch data in response to these changes.
Responding to user input
Imagine a scenario where we want to show search results as the user types into a search box:
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
{{ result }}
</div>
`,
})
export class App {
// We are using a FormControl to watch the user's input
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMap(keywords => this.http.get('/api/search', {params: { q: keywords }))
);
private http = inject(HttpClient);
}
In this example, searchControl.valueChanges
is an Observable. This Observable sends out an event each time the user changes the text in the search box. We then use something called the switchMap
operator from RxJS. This operator lets us make a fresh data request each time the user changes the text. It also makes sure we only work with the results for the latest text input. This is very important for keeping your app speedy and pleasant for the user.
We use *ngIf
with the async
pipe so that the template automatically connects to our result$
Observable and updates itself. This makes sure that the template always shows the newest search results to the user in real-time as they type into the search box.
Managing loading and error states with RxJS
A key part of getting data is giving feedback to the user during the entire loading process. This could be a loading message or an error message. Instead of using separate isLoading
or hasError
variables in our components, we'll use some RxJS operators called map
, catchError
, startWith
, and scan
.
Let's have a look at the following example:
interface LoadingState<T = unknown> {
loading: boolean;
error?: Error | null;
data?: T;
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
<div *ngIf="result.loading">Loading...</div>
<div *ngIf="result.data as data">{{data}}</div>
<div *ngIf="result.error as error">{{error.message}}</div>
</div>
`,
})
export class App {
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMap((value) =>
this.http.get('/api/search', {params: { q: value }).pipe(
map((data) => ({ data, loading: false })),
catchError((error) => of({ error, loading: false })),
startWith({ error: null, loading: true })
)
),
scan((state: LoadingState<string>, change: LoadingState<string>) => ({
...state,
...change,
}))
);
private http = inject(HttpClient);
}
In the code above:
We use
map
to change the raw data from the HTTP request into an object. This object includes the data and aloading
flag that is set tofalse
.We use
catchError
to manage any errors that happen during the HTTP request. If an error happens, it gives back an Observable with an object. This object includes the error and aloading
flag that is set tofalse
.We use
startWith
to begin the stream with a specific value. Here, it starts with an object that includes anull
error and aloading
flag set totrue
. This makes sure that the user sees a loading state before any data is loaded.scan
works likeArray.reduce
. It takes the previous state and the new changes and mixes them into a new state. Here, it takes the previous loading state and blends it with the changes from the HTTP request result. This way, we keep a constant stream of state changes, each adding in the changes of the one before it.Now, in your template, you can use Angular's async pipe to subscribe to
result$
, and use*ngIf
to show different parts of the template based on the loading state.
These operators look complicated! Why not just use separate isLoading and hasError variables?
Even if it looks easier to use this.isLoading
or this.hasError
variables in your component, using RxJS operators gives you better control.
When your loading and error states are inside the data stream, they're tied to the loading process. They can't be changed by anything else. This prevents unexpected changes and bugs.
If you use separate variables, other parts of your app could change them. This could lead to bugs that are hard to track down and fix.
Keeping these states inside the data stream makes your code less complicated and safer. It also makes sure the user interface updates smoothly.
Important: keep it DRY! Create your own reusable RxJS Operator
You probably need the pattern we discussed above in multiple places in your app. But you donβt want to write the same code over and over again. Luckily, it's not hard to make your own RxJS operators! We can take the code we used before and make a useful RxJS operator that we can use all over our app.
Let's look at an example: we'll make our own operator called switchMapWithLoading
. We'll put it in a separate file named switch-map-with-loading.ts
.
// switch-map-with-loading.ts
interface LoadingState<T = unknown> {
loading: boolean;
error?: Error | null;
data?: T;
}
export function switchMapWithLoading<T>(
observableFunction: (value: any) => Observable<T>
): OperatorFunction<any, LoadingState<T>> {
return (source: Observable<any>) =>
source.pipe(
switchMap((value) =>
observableFunction(value).pipe(
map((data) => ({ data, loading: false })),
catchError((error) => of({ error, loading: false })),
startWith({ error: null, loading: true })
)
),
scan((state: LoadingState<T>, change: LoadingState<T>) => ({
...state,
...change,
}))
);
}
You can then import it in your component and use it like so:
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
<div *ngIf="result.loading">Loading...</div>
<div *ngIf="result.data as data">{{data}}</div>
<div *ngIf="result.error as error">{{error.message}}</div>
</div>
`,
})
export class App {
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMapWithLoading((value) => this.http.get('/api/search', {params: { q: value })))
);
private http = inject(HttpClient);
}
With this, you've made your Angular application code more efficient and maintainable by abstracting the reactive data fetching mechanism into a custom RxJS operator, which you can use across your application. This will not only keep your code DRY (Don't Repeat Yourself) but also make it easier to understand and manage.
Making it even simpler with *ngxLoadWith
Now, you might be wondering if there's a simpler way to handle all of this without dealing with complex RxJS operators. Good news! Introducing the *ngxLoadWith
directive, which provides a simpler approach to managing loading states in your Angular application.
To get started, you need to install the *ngxLoadWith
package. Run the following command:
npm install ngx-load-with
Once installed, you can use the *ngxLoadWith
directive in your Angular component like this:
@Component({
selector: "my-app",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, NgxLoadWithModule],
template: `
<input [formControl]="searchControl" />
<div
*ngxLoadWith="
getResult as data;
args: searchControl.value;
loadingTemplate: loading;
errorTemplate: error
"
>
{{ data }}
</div>
<ng-template #loading>Loading...</ng-template>
<ng-template #error let-error>{{ error.message }}</ng-template>
`,
})
export class App {
searchControl = new FormControl("");
getResult = (keywords: string) =>
this.http.get("/api/search", { params: { q: keywords } });
private http = inject(HttpClient);
}
Let's break it down. We simply define a function called getResult
that takes a keywords
parameter and returns an HTTP request. We then pass this function to the *ngxLoadWith
directive, along with the value of searchControl.value
as the args
input.
Now whenever the searchControl
value changes, the getResult
function will be called with the new value. The *ngxLoadWith
directive will then subscribe to the result of the function and display the result in the template.
Then we define two templates: loading
and error
. These templates will be automatically displayed when the getResult
function is loading or has an error.
That's it! You can now easily and safely manage loading states in your Angular application without having to deal with complex RxJS operators.
Ready to give it a try? Check out the source code at github.com/rensjaspers/ngx-load-with. If you find *ngxLoadWith
helpful, don't forget to give it a star on Github. We would love to hear your feedback and experiences with this directive!
Top comments (0)