Skip to content

Latest commit

 

History

History

operators

@jscutlery/operators

This package regroups a couple of RxJS operators meant to simplify some common patterns.

Installation

yarn add @jscutlery/operators

# or

npm install @jscutlery/operators

suspensify

When dealing with an asynchronous source of data in a web application, it is a common requirement to display different content depending on the following states:

  • pending: the data is being fetched
  • error: an error occurred while fetching the data
  • success: some data has been fetched
  • finalized: there is no more data to fetch or an error happened (it is called finalized and not completed to avoid confusion with RxJS complete event which only happens if there is no error).

The most common ways of implementing this are error-prone as they are either based on a complex combination of native RxJS operators or side effects that break the reactivity chain.

The suspensify operator is meant to provide a simple and efficient way of dealing with data fetching by providing deriving a state from the source observable.

Goals

  • ⚡️ simplify the implementation of asynchronous data fetching
  • 🎬 know when the data is being fetched and show some loading indicator
  • 🐞 avoid common mistakes like showing data and the last error simultaneously
  • 💥 simplify and encourage error-handling

Usage

Strict mode (default)

Thanks to strict mode the emitted suspense can be narrowed down.

interval(1000)
  .pipe(take(2), suspensify())
  .subscribe((suspense) => {
    suspense.value; // 💥
    suspense.error; // 💥
    if (suspense.hasValue) {
      suspense.value; // ✅
      suspense.error; // 💥
    }
    if (suspense.hasError) {
      suspense.value; // 💥
      suspense.error; // ✅
    }
  });
{
  finalized: false,
  hasError: false,
  hasValue: false,
  pending: true,
}
{
  finalized: false,
  hasError: false,
  hasValue: false,
  pending: false,
  value: 0,
}
...
{
  finalized: true,
  hasError: false,
  hasValue: true,
  pending: false,
  value: 1,
}

Lax mode

interval(1000)
  .pipe(take(2), suspensify({strict: false}))
  .subscribe((data) => console.log(data));
{
  finalized: false,
  hasError: false,
  hasValue: false,
  pending: true,
  value: undefined,
  error: undefined,
}
{
  finalized: false,
  hasError: false,
  hasValue: false,
  pending: false,
  value: 0,
  error: undefined,
}
...
{
  finalized: true,
  hasError: false,
  hasValue: true,
  pending: false,
  value: 1,
  error: undefined,
}

With Angular

@Component({
  template: `
    <ng-container *ngIf="suspense$ | async as suspense">

      <my-spinner *ngIf="suspense.pending"></my-spinner>

      <div *ngIf="suspense.hasError">
        {{ suspense.error }} // ✅
        {{ suspense.value }} // 💥 will not compile in strict mode
      </div>

      <div *ngIf="suspense.hasValue">
        {{ suspense.error }} // 💥 will not compile in strict mode
        {{ suspense.value }} // ✅
      </div>

    </ng-container>
  `,
})
export class MyComponent {
  suspense$ = this.fetchData().pipe(suspensify());
  ...
}

With @rx-angular/state

@Component({
  template: `
    <my-spinner *ngIf="pending$ | async"></my-spinner>

    <div *ngIf="error$ | async as error">{{ error }}</div>

    <div *ngIf="value$ | async" as value>{{ value }}</div>
  `,
})
export class MyComponent {
  value$ = this.state.select('beer', 'value');
  error$ = this.state.select('beer', 'error');
  pending$ = this.state.select('beer', 'pending');

  constructor(private state: RxState<{ beer: Suspense<'🍻'> }>) {
    this.state.connect('beer', this.fetchBeer());
  }
}

Alternatives

F.A.Q.

How does it defer from materialize?

materialize doesn't produce a derived state. In fact, the C (complete) event doesn't contain the last emitted value. Also, materialized doesn't trigger a pending event so the observable doesn't emit anything before the first value is emitted or an error occurs or the source completes.

Roadmap

  1. mergeSuspense function should merge multiple sources in one state that contains the global state of all sources and each one of them.