Observable State
Tags: angular, rxjs, typescript
Published: 2023-05-26 14:16:19


Hi there 👋! A few months ago, I shared my approach to managing Observable state on Reddit, maybe some of you are doing the same, maybe not, anyway, I noticed that people liked it, so I decided to write a small article on Dev.to.

## What's it about?
The article will cover how to add Observable state, which is sometimes necessary to display a **preloader**, an **error**, or the **result** of a HTTP request.

Having worked in different companies and teams, I've seen various approaches: complex models, pure functions, classes, and more. But in my opinion, if the task sounds simple, its implementation should be simple too!

## Let's get started.

### Requirements
So, first, let's imagine that we have a server request that returns a list of fruits. It can be complex or simple, but the main things we want to achieve are as follows:

- Display a **preloader** while the request is in progress.
- Display an **error** in the template with a retry button if the request fails.
- Display the **result** if everything is okay.

### Coding
Let's start by creating the desired interface for our state.

```typescript
export interface ObservableState<T, E = Error> {
  result: T | null;
  error: E | null;
  pending: boolean;
}
```

As you can see, the interface is quite simple, containing only what we need and nothing more!

Now let's talk about creating the state. The most important aspect here is simplicity and reusability, which is why a custom RxJS operator will be a perfect fit!

```typescript
import type { Observable, OperatorFunction } from 'rxjs';
import { of } from 'rxjs';
import { map, catchError, switchMap, startWith, tap } from 'rxjs/operators';

export interface ObservableState<T, E = Error> {
  result: T | null;
  error: E | null;
  pending: boolean;
}

export function observableState<T, E = Error>(
  retry?: Observable<unknown>
): OperatorFunction<T, ObservableState<T, E>> {
  return (source: Observable<T>) => {
    // Default state
    let state: ObservableState<T, E> = {
      result: null,
      error: null,
      pending: false,
    };

    return (retry?.pipe(startWith(null)) ?? of(null))
      .pipe(
        switchMap(() =>
          source.pipe(
            // Map result of observable
            map((result: T) => ({ result, pending: false })),
            // Map error of observable
            catchError((error: E) => of({ result: null, error, pending: false })),
            // Start from pending state and clear error
            startWith({ error: null, pending: true }),
            // Merge the current state with new state and return it
            map((updatedState: Partial<ObservableState<T, E>>) =>
              (state = { ...state, ...updatedState })
            )
          )
        )
      );
  };
}
```

So, as you can see, our operator takes a `retry` Observable as input. If it is provided, when it emits, we will restart the source observable. Then, we simply map the **result** or **error** and merge it with the main state inside the operator.

As a result, we will have the following behavior:

- When the observable starts, we always start with `preloader: true`, while clearing only the **error**, so that if it's an update action, our current **result** doesn't disappear.
- If an **error** occurs, we clear the **result** and **preloader**, and return only the state with the **error**.
- When we receive a **result**, we turn off the **preloader**.

Now let's take a look at an example of how to use it:

```typescript
import { Component, VERSION, ChangeDetectionStrategy } from '@angular/core';
import { Observable, of, Subject, switchMap, throwError } from 'rxjs';
import { delay } from 'rxjs/operators';
import { observableState, ObservableState } from './observable-state';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  // Just to imitate an error for the HttpRequest
  imitateError: boolean = false;

  // To retry the HttpRequest
  retry$: Subject<void> = new Subject<void>();

  // Imitation of the HttpRequest
  httpRequest$: Observable<string[]>;

  // Stated version of the httpRequest$
  statedHttpRequest$: Observable<ObservableState<string[]>>;

  constructor() {
    this.httpRequest$ = of(['Apple', 'Orange', 'Kiwi', 'Pineapple']).pipe(
      delay(2000),
      switchMap((result: string[]) =>
        this.imitateError
          ? throwError(() => new Error('Something terrible has happened! 👻'))
          : of(result)
      )
    );

    // Add state to the HttpRequest
    this.statedHttpRequest$ = this.httpRequest$.pipe(
      observableState(this.retry$)
    );
  }
}
```

As you can see, it's quite simple. We just add our `observableState` operator along with the `Subject` we created to the existing Observable. This allows us to retry the request in case of an error or for data refresh. Now let's take a look at how it looks in the template:

```html
<section *ngIf="statedHttpRequest$ | async as state">
  <div class="header">
    <h1>Stated Data</h1>
    <button (click)="retry$.next()">retry</button>
    <label>
      <input type="checkbox" [(ngModel)]="imitateError" />
      Imitate an error
    </label>
  </div>

  <div *ngIf="state.pending">Loading...</div>

  <ul *ngIf="state.result">
    <li *ngFor="let fruit of state.result">{{ fruit }}</li>
  </ul>

  <div class="error" *ngIf="state.error">{{ state.error }}</div>
</section>
```

Again, it's simple and straightforward. We have a single state, and based on it, we display different information to the user.

## Can we make it better?
Of course! Because there's no limit to perfection! 😃
Often, such states are only displayed in the template, so why do we need code in the TypeScript file, right? Right! Let's create a pipe!

```typescript
import { Pipe, PipeTransform } from '@angular/core';
import { Observable } from 'rxjs';
import { observableState, ObservableState } from './observable-state';

@Pipe({
  name: 'observableState'
})
export class ObservableStatePipe implements PipeTransform {
  transform<T, E = Error>(obs: Observable<T>, retry?: Observable<unknown>): Observable<ObservableState<T, E>> {
    return obs.pipe(observableState(retry));
  }
}
```

Now we can add the state directly in the template!

```html
<!-- Just add it like this -->
<section *ngIf="httpRequest$ | observableState: retry$ | async as state">
  <div class="header">
    <h1>Stated Data via Pipe</h1>
    <button (click)="retry$.next()">retry</button>
    <label>
      <input type="checkbox" [(ngModel)]="imitateError" />
      Imitate an error
    </label>
  </div>

  <div *ngIf="state.pending">Loading...</div>

  <ul *ngIf="state.result">
    <li *ngFor="let fruit of state.result">{{ fruit }}</li>
  </ul>

  <div class="error" *ngIf="state.error">{{ state.error }}</div>
</section>
```

## Live example

{% embed https://stackblitz.com/edit/observable-state?embed=1&file=src%2Fapp%2Fapp.component.html&view=preview %}

## It's time to say goodbye 👋
That's all folks! I hope my approach was helpful to you! You can also play around with it on [StackBlitz](https://stackblitz.com/edit/observable-state)