Reactive Loading State Management with RxJS and Angular: Loading Stuff Based on Changes to Other Stuff
Tags: rxjs, angular, webdev
Published: 2023-07-06 16:03:00


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:

```typescript
@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:

```typescript
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:

1. We use `map` to change the raw data from the HTTP request into an object. This object includes the data and a `loading` flag that is set to `false`.

2. 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 a `loading` flag that is set to `false`.

3. We use `startWith` to begin the stream with a specific value. Here, it starts with an object that includes a `null` error and a `loading` flag set to `true`. This makes sure that the user sees a loading state before any data is loaded.

4. `scan` works like `Array.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.

5. 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`.

```typescript
// 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:

```typescript
@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`](https://github.com/rensjaspers/ngx-load-with) 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:

```typescript
@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](https://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!