ngx-remotedata icon indicating copy to clipboard operation
ngx-remotedata copied to clipboard

💥 Synchronize data fetching and UI state

RemoteData

Slaying a UI Antipattern with Angular.

Build Status npm version npm downloads

Library inspired by Kris Jenkins blog post about How Elm slays a UI antipattern, which mixes pretty well with another article written by Scott Hurff about what he calls the UI Stack.

Table Of Contents

  • What we are trying to solve
    • The traditional approach
    • The RemoteData approach
  • Installation
  • Basic Usage
  • Examples
    • Demo 👀
    • Basic
    • Services
    • Ngrx
  • Api 📚
  • Pipes 📚

What we are trying to solve

We are making an API request and want to display different things based on the request's status.

The traditional approach

export interface SunriseSunset {
  isInProgress: boolean;
  error: string;
  data: {
    sunrise: string;
    sunset: string;
  };
}

Let us see what each property means:

  • isInProgress: It is true while the data is being fetched.
  • error: It is either null (no errors) or any string (there are errors).
  • data: Either null (no data) or the result payload (there is data).

There are a few problems with this approach, the main one being that it is possible to create invalid states such as:

{
  "isInProgress": true,
  "error": "Fatal error",
  "data": {
    "sunrise": "I am good data.",
    "sunset": "I am good data too!"
  }
}

Our html template will have to use complex *ngIf statements to make sure we are displaying the correct information.

The RemoteData approach ™

Instead of using a complex data structures we use a single data type to express all possible request states:

type RemoteData<T, E> = NotAsked | InProgress<T> | Failure<E, T> | Success<T>;

This approach makes it impossible to create invalid states.

Installation

npm install --save ngx-remotedata

Basic Usage

// app.module.ts

import { RemoteDataModule } from 'ngx-remotedata';

@NgModule({
  imports: [
    // (...)
    RemoteDataModule
  ]
})
// app.component.ts

import {
  RemoteData,
  inProgress,
  notAsked,
  success,
  failure,
} from 'ngx-remotedata';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  remoteData: RemoteData<string> = notAsked();

  setNotAsked() {
    this.remoteData = notAsked();
  }

  setInProgress() {
    this.remoteData = inProgress('In progress...');
  }

  setSuccess() {
    this.remoteData = success('Success!');
  }

  setFailure() {
    this.remoteData = failure('Wrong!');
  }
}
<!-- app.component.html -->

<ul>
  <li><button (click)="setNotAsked()">Not Asked</button></li>
  <li><button (click)="setInProgress()">InProgress</button></li>
  <li><button (click)="setSuccess()">Success</button></li>
  <li><button (click)="setFailure()">Failure</button></li>
</ul>

<hr />

<h4 *ngIf="remoteData | isNotAsked">Not Asked</h4>
<h4 *ngIf="remoteData | isInProgress">InProgress...</h4>
<h4 *ngIf="remoteData | isSuccess" style="color: green">
  {{ remoteData | successValue }}
</h4>
<h4 *ngIf="remoteData | isFailure" style="color: red">
  {{ remoteData | failureValue }}
</h4>

Examples

Demo 👀

Source code

  • The basics
  • Plain old services
  • Ngrx (includes store rehydration from localStorage)

Api

RemoteData 📚

RemoteData<T, E>

RemoteData is used to annotate your request variables. It wraps all possible request states into one single union type. Use the parameters to specify:

  • T: The success value type.

  • E: The error value type (string by default).

  • Type guard function: isRemoteData = <T, E>(value: unknown): value is RemoteData<T, E>.

NotAsked 📚

  • Constructor function: notAsked<T, E>(): RemoteData<T, E>.
  • Type guard function: isNotAsked<T, E>(value: unknown): value is NotAsked.

When a RemoteData is a NotAsked instance, it means that the request hasn't been made yet.

type User = { email: string };
const myRemoteData: RemoteData<User> = notAsked();

// (...)

if (isNotAsked(myRemoteData)) {
  // Here myRemoteData is narrowed to NotAsked
}

InProgress 📚

  • Constructor function: inProgress<T, E>(value?: T): RemoteData<T, E>.
  • Type guard function: isInProgress<T, E>(value: unknown): value is InProgress<T>.

When a RemoteData is an InProgress instance, it means that the request has been made, but it hasn't returned any data yet.

The InProgress instance can contain a value of the same T type as Success. Useful when you want to use the last Success value while the new data is being fetched.

type User = { email: string };
const myRemoteData: RemoteData<User> = inProgress({ email: '[email protected]' });

// (...)

if (isInProgress(myRemoteData)) {
  // Here myRemoteData is narrowed to InProgress
  console.log(`I have some data: ${myRemoteData.value.email}`);
}

Success 📚

  • Constructor function: success<T, E>(value: T): RemoteData<T, E>.
  • Type guard function: isSuccess<T, E>(value: unknown): value is Success<T>.

When a RemoteData is a Success instance, it means that the request has completed successfully and the new data (of type T) is available.

type User = { email: string };
const myRemoteData: RemoteData<User> = success({ email: '[email protected]' });

// (...)

if (isSuccess(myRemoteData)) {
  // Here myRemoteData is narrowed to Success
  console.log(`I have some data: ${myRemoteData.value.email}`);
}

Failure 📚

  • Constructor function: failure<T, E>(err: E, val?: T): RemoteData<T, E>.
  • Type guard function: isFailure<T, E>(value: unknown): value is Failure<E, T>.

When a RemoteData is a Failure instance, it means that the request has failed. You can get the error information (of type E) from the payload.

The Failure instance can contain a value of the same T type as Success. Useful when you want to use the last Success value while displaying the failure message.

type User = { email: string };
const myRemoteData: RemoteData<User> = failure('Something went wrong.', {
  email: '[email protected]',
});

// (...)

if (isFailure(myRemoteData)) {
  // Here myRemoteData is narrowed to Failure
  console.log(`This is the failure: ${myRemoteData.error}`);
  console.log(`I have some data: ${myRemoteData.value.email}`);
}

The default type for errors is string, but you can also provide other types like Error:

type User = { email: string };
const myRemoteData: RemoteData<User, Error> = failure(
  new Error('Something went wrong.')
);

Unwrapping RemoteData values

getOrElse 📚

getOrElse<T, E>(rd: RemoteData<T, E>, defaultValue: T): T;

getOrElse unwraps and returns the value of Success instances or the defaultValue when it's any other RemoteData variant.

// Example
let myRemoteData = success('ok!');
console.log(getOrElse(myRemoteData, 'The default value')); // ok!

myRemoteData = failure('There has been an error');
console.log(getOrElse(myRemoteData, 'The default value')); // The default value

fold 📚

fold<T, E>(
  onNotAsked: () => T,
  onInProgress: (value: T | undefined) => T,
  onFailure: (error: E, value: T | undefined) => T,
  onSuccess: (value: T) => T,
  rd: RemoteData<T, E>
): T;

With fold you unwrap the RemoteData value by providing a function for each of the type variants.

// Example
const rd = success('this is fine!');
const result = fold(
  () => 'not asked',
  (val) => 'in progress: ' + val,
  (error, value) => `failure: ${error} ${value}`,
  (value) => 'success: ' + value,
  rd
);
console.log(result); // success: this is fine!

Transforming RemoteData values

map 📚

map<A, B, E>(
  fn: (a: A) => B,
  rd: RemoteData<A, E>
): RemoteData<B, E>;

With map you provide a transformation function that is applied to a RemoteData only when it's a Success instance.

// Example
const scream = (s: string) => s.toUpperCase();
const hello = success('hello!');
const helloScreaming = map(scream, hello);
console.log(helloScreaming); // success('HELLO!')

mapFailure 📚

mapFailure<A, E, F>(
  fn: (e: E) => F,
  rd: RemoteData<A, E>
): RemoteData<A, F>;

With mapFailure you provide a transformation function that is applied to a RemoteData only when it's a Failure instance.

// Example
const scream = (s: string) => s.toUpperCase();
const error = failure('wrong!');
const wrongScreaming = mapFailure(scream, error);
console.log(wrongScreaming); // failure('WRONG!')

chain 📚

chain<A, B, E>(
  fn: (a: A) => RemoteData<B, E>,
  rd: RemoteData<A, E>
): RemoteData<B, E>;

With chain you can provide a transormation function that can change the returned RemoteData variant.

// Example
const checkAge = (n: number) =>
  n >= 0 ? success(n) : failure(`${n} is an invalid age`);
let ageResult = chain(checkAge, success(25));
expect(ageResult).toEqual(success(25));
ageResult = chain(checkAge, success(-3));
expect(ageResult).toEqual(failure('-3  is an invalid age'));

RxJs operators

filterSuccess 📚

Specialized version of the rxjs filter operator for RemoteData values. Emits only when source Observable is a Success, also narrows the emitted value to Success.

// Example
const myRemoteData = success(3);
of(myRemoteData)
  .pipe(
    filterSuccess(),
    map((s) => s.value * 2)
  )
  .subscribe((n) => {
    console.log(n); // 6
  });

filterFailure 📚

Specialized version of the rxjs filter operator for RemoteData values. Emits only when source Observable is a Failure, also narrows the emitted value to Failure.

// Example
const myRemoteData = failure('wrong!');
of(myRemoteData)
  .pipe(
    filterFailure(),
    map((f) => 'Error: ' + f.error)
  )
  .subscribe((err) => {
    console.log(err); // 'Error: wrong!'
  });

Pipes

isNotAsked 📚

transform<T, E>(rd: RemoteData<T, E>): boolean;

Returns true when RemoteData is a NotAsked instance.

anyIsNotAsked 📚

transform<T, E>(
  rds$: Observable<RemoteData<T, E>>[]
): boolean;

Returns true when any RemoteData<T, E>[] items is a NotAsked instance.

isInProgress 📚

transform<T, E>(rd: RemoteData<T, E>): boolean;

Returns true when RemoteData is an InProgress instance.

anyIsInProgress 📚

transform<T, E>(
  rds$: Observable<RemoteData<T, E>>[]
): boolean;

Returns true when any RemoteData<T, E>[] item is an InProgress instance.

isFailure 📚

transform<T, E>(rd: RemoteData<T, E>): boolean;

Returns true when RemoteData is a Failure instance.

isSuccess 📚

transform<T, E>(rd: RemoteData<T, E>): boolean;

Returns true when RemoteData is a Success instance.

hasValue 📚

transform<T, E>(rd: RemoteData<T, E>): boolean;

Returns true when RemoteData is a Success instance or is an InProgress or Failure instance with a value that is not null or undefined.

successValue 📚

transform<T, E>(
  rd: RemoteData<T, E>,
  defaultValue?: T
): T | undefined;

Returns the Success payload (of type T) when the RemoteData is a Success instance, otherwise it returns the defaultValue when provided or undefined when not.

inProgressValue 📚

transform<T, E>(
  rd: RemoteData<T, E>,
  defaultValue?: T | undefined
): T | undefined;

Returns the InProgress payload (of type T) when RemoteData is an InProgress instance, otherwise it returns the provided defaultValue or undefined when not.

remoteDataValue 📚

transform<T, E>(rd: RemoteData<T, E>): T | E | undefined;

Returns the InProgress, Failure or Success payload (of type T) when RemoteData is an InProgress, Failure or Success instance. Returns undefined otherwise.

failureError 📚

transform<T, E>(rd: RemoteData<T, E>): E | undefined

Returns the Failure error payload (of type E) when RemoteData is a Failure instance or undefined otherwise.

failureValue 📚

transform<T, E>(rd: RemoteData<T, E>): T | undefined

Returns the Failure payload (of type T) when RemoteData is a Failure instance or undefined otherwise.