This package regroups a couple of RxJS operators meant to simplify some common patterns.
yarn add @jscutlery/operators
# or
npm install @jscutlery/operators
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 notcompleted
to avoid confusion with RxJScomplete
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.
- ⚡️ 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
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,
}
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,
}
@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());
...
}
@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());
}
}
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.
mergeSuspense
function should merge multiple sources in one state that contains the global state of all sources and each one of them.