Slaying a UI Antipattern with Angular.
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.
You are making an API request, and you want to display different things based on the status of the request.
export interface SunriseSunset {
isInProgress: boolean;
error: string;
data: {
sunrise: string;
sunset: string;
};
}
Let’s see what each property means:
isInProgress
: It‘s true while the remote data is being fetched.error
: It‘s either null (no errors) or any string (there are errors).data
: It’s either null (no data) or an object (there is data).
There are a few problems with this approach but the main one is that it is possible to create invalid states such:
{
isInProgress: true,
error: 'Fatal error',
data: {
sunrise: 'I am good data.',
sunset: 'I am good data too!',
}
}
Our template will have to use complex *ngIf
statements to make sure that we are displaying precisely what we should.
Instead of using a complex object we use a single data type to express all possible request states:
export type RemoteData<T, E = string> =
| NotAsked
| InProgress<T>
| Failure<E>
| Success<T>;
This approach makes it impossible to create invalid states.
npm install --save ngx-remotedata
// app.module.ts
import { RemoteDataModule } from 'ngx-remotedata';
@NgModule({
imports: [
// (...)
RemoteDataModule
]
})
// app.component.ts
import { InProgress, NotAsked, Success, Failure } from 'ngx-remotedata';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
remoteData: RemoteData<string> = NotAsked.of();
setNotAsked() {
this.remoteData = NotAsked.of();
}
setInProgress() {
this.remoteData = InProgress.of('In progress...');
}
setSuccess() {
this.remoteData = Success.of('Success!');
}
setFailure() {
this.remoteData = Failure.of('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>
RemoteData<T, E = string>
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).
NotAsked
When a RemoteData
is an instance of the NotAsked
class, it means that the request hasn't been made yet.
type User = { email: string };
const remoteData: RemoteData<User> = NotAsked.of();
InProgress<T>
When a RemoteData
is an instance of the InProgress
class, it means that the request has been made, but it hasn't returned any data yet. The InProgress
class can contain a value of the same T
type as the Success
class. Useful when you want to use the last Success
value while the new data is being fetched.
type User = { email: string };
const remoteData: RemoteData<User> = InProgress.of({ email: '[email protected]' });
Success<T>
When a RemoteData
is an instance of the Success
class, it means that the request has completed successfully and the new data (of type T
) is available.
type User = { email: string };
const remoteData: RemoteData<User> = Success.of({ email: '[email protected]' });
Failure<E>
When a RemoteData
is an instance of the Failure
class, it means that the request has failed. You can get the error information (of type E
) from the payload.
type User = { email: string };
const remoteData: RemoteData<User> = Failure.of('Something went wrong.');
The default type for errors is string
, but you can also provide other types like Error
:
type User = { email: string };
const remoteData: RemoteData<User, Error> = Failure.of(
new Error('Something went wrong.')
);
isNotAsked | RemoteData<any> : boolean
Returns true when RemoteData
is a NotAsked
instance.
isInProgress | RemoteData<any> : boolean
Returns true when RemoteData
is a InProgress
instance.
anyIsInProgress | Observable<RemoteData<any>>[] : boolean
Returns true when any item in RemoteData[]
is a InProgress
instance.
isFailure | RemoteData<any> : boolean
Returns true when RemoteData
is a Failure
instance.
isSuccess | RemoteData<any> : boolean
Returns true when RemoteData
is a Success
instance.
successValue | RemoteData<T> : (T | undefined)
Returns the Success
payload (of type T
) when RemoteData
is a Success
instance or undefined
instead.
inProgressValue | RemoteData<T> : (T | undefined)
Returns the InProgress
payload (of type T
) when RemoteData
is a InProgress
instance or undefined
instead.
failureValue | RemoteData<T, E> : (E | undefined)
Returns the Failure
payload (of type E
) when RemoteData
is a Failure
instance or undefined
instead.