Part of the 𝗥𝘅𝑓𝑥 family of libraries.
30 years after setTimeout
introduced the world to the asynchronous nature of JavaScript, effect execution is still clumsy at best, and broken in many cases. And none of the popular front-end solutions (Angular, React, RxJS) present a complete solution that deals with all the concerns of async effects in a framework-independent way.
- Error handling that is predictable, and does not compromise the integrity of the app the more effects you add.
- Automatic
loading
/active
state tracking. - Automatic tracability of all lifecycle events of effects (
started
,next
,complete
,error
,canceled
, etc.) - Simple effect cancelation from anywhere inside or outside the component tree.
- Seamless interopation with Promises, Observables, Iterables and generators.
- Easily reduced or adjustable concurrency (immediate, queueing, throttling, etc) without introducing complication or additional variables.
An 𝗥𝘅𝑓𝑥 service (or a bus) is a view-framework-independent, pure JS container for Effect Management and State Mangement, based on RxJS. An 𝗥𝘅𝑓𝑥 Service supports all the above pain points in an easy API.
- You notice you are introducing
loading
state fields which must be set and unset manually - You are manually outputting logging messages, and there is no standard convention between them.
- You are using framework-specific constructs (
useEffect
, async pipe) to manage asynchrony. - You want a better separation of the View Layer from the async layer.
- You are dealing with race conditions
- You are using RxJS, but want fewer imports and operators, and you're feeling it clumsy to manage subscriptions in addition to Observables.
- You are using React, and want to heed the warnings in their docs about
useEffect
being used often in the wrong ways. - You are tired of async errors breaking the view layer, or the app as a whole, as more effects get added to your app.
- You find tests take too long to run when they have to be called through the view layer, and you want something that is testable independent of the view.
In short - if you believe there is a more concise, more airtight, race-condition-proof way to do async, you may have found it right here in an 𝗥𝘅𝑓𝑥 service or bus listener.
This will create a counter that incremements after 1000 msec, queueing up future increments if one is already in progres.
import { createQueueingService } from '@rxfx/service'
const initialState = 0
const asyncCounter = createQueueingService<void, void, Error, number>(
'count',
() => after(1000),
({ isResponse }) => (state = initialState, event) => {
if (isResponse(event)) {
return state + 1
}
return state;
}
);
Although there are APIs for framework integration (see @rxfx/react
), in pure JS the service is used like this:
// Request a counter increment (will complete after 1000 msec)
asyncCounter.request();
// Request a counter increment, with a Promise for when done
asyncCounter.send().then(() => /* ... */)
// Cancel any current executions of the effect, and/or queued
asyncCounter.cancelCurrent()
asyncCounter.cancelCurrentAndQueued()
// Update your UI state
asyncCounter.state.subscribe(count => /* ... */)
// Update your loading indicator
asyncCounter.isActive.subscribe(isActive => /* ... */)
// Display an error which is self-clearing
asyncCounter.currentError.subscribe(e => /* ... */)
// Consume returned values, or progress notifications of the effect
asyncCounter.responses.subscribe(resp => /* ... */)
Because RxFx is RxJS, it is easy to integrate into Angular. You'll be happy to no longer have to track loading
variables in state, and have a simpler API to both cancelation and concurrency.
A component need only import (or inject) a service, bus, or effect, and it can expose properties and methods to a template trivially:
import { counterService } from '../services/counter.service';
@Component({
selector: 'app-my-counter',
templateUrl: './my-counter.component.html',
})
export class MyCounterComponent {
count$: Observable<number>;
isActive$: Observable<boolean>;
constructor() {
this.count$ = counterService.state;
this.isActive$ = counterService.isActive;
}
increment() {
counterService.request(1);
}
decrement() {
counterService.request(-1);
}
reset() {
counterService.reset();
}
}
Play with the CodeSandbox.
The following examples and tutorials will give you a feel for what UX you can build, and the ease of DX you'll find.
It doesn't take more than a simple time-delayed async counter to show all the subtleties of async, and how 𝗥𝘅𝑓𝑥 lets you control them:
-
Synchronous Counter - By having an empty effect, and a reducer that increments the count upon a request, this synchronous service has the exact same architecture as an asynchronous one.
-
Asynchronous Counter with loading and cancelability, default concurrency. This service shows a loading state, and allows cancelation. It allows multiple increments concurrently, and the reducer increments on the response side of the delay.
-
Asynchronous Counter, queued, with cancel-on-unmount By simply modifying
createService
tocreateQueueingService
, this service guarantees no more than one increment is in progress at a time. On unmount, all current and queued increments are canceled. -
Asynchronous Counter, with progress indicator. By wrapping the handler in
monitorHandler
, we can update the UI with the percent completed, without affecting what's been written previously. Note: this is a compatibility-approach for when an effect doesn't provide its own progress updates - for example any Promise returning function -
Asynchronous Counter, with cancelable fetch Instead of a mere delay, this service awaits a real
fetch
to a delayed endpoint at httpbin.org. It usesmakeAbortableHandler
to obtain and pass asignal
that will fully cancel the fetch when the service is told to cancel.
The counter is one example of the 7 GUIs benchmark for building UI applications. In the 7 GUIs, different frameworks can be compared against how elegantly they solve the building of 7 GUIs of increasing complexity.
𝗥𝘅𝑓𝑥 has been used to build all 7 GUIs, the counter being the first one. The remaining, with links to live versions are:
- Temperature. Two-way binding between fields.
- Flight Booker Constraints, validating inputs.
- Timer Concurrency, competing user/signal interactions, responsiveness.
- CRUD Managing a list, mutation.
- Circles Canvas drawing, modal interaction
- Spreadsheet Change propogation, a mini-language the user can input.
Race conditions are easily prevented when code is set to run in the correct Concurrency Mode for its use case. With 𝗥𝘅𝑓𝑥, its easily named and tested modes (which use RxJS operators underneath) allow you to keep your code readable, and you can eliminate race conditions in a 1-line code diff.
The modes, pictorially represented here with use cases and descriptions, are utilized just by calling createService
, createQueueingService
, createSwitchingService
, or createBlockingService
accordingly. Your effect stays the same, only the concurrency is different.
Choose your mode by answering this question:
If the effect is running, and a new request arrives, should the service:
- Begin the new effect at once, allowing both to finish in any order. (
createService
) - Begin the new effect only after any currently running effects, preserving order. (
createQueueingService
) - Prevent/throttle the new effect from beginning. (
createBlockingService
) - Cancel the currently running effect and begin the new effect at once. (
createSwitchingService
)
And one final mode, seldom used, but included for completion:
- Cancel the currently running effect, and don't begin a new effect. (
createTogglingService
)
Here are representations of each mode:
Download SVG
For more information about what went into 𝗥𝘅𝑓𝑥, the following are great reads.
- CQRS - The architectural separation that makes 𝗥𝘅𝑓𝑥 possible
- RxJS - the awesomely-capable async library that 𝗥𝘅𝑓𝑥 is built from, and neatly abstracts away for most use cases.
- Ember Concurrency - The most elegant API to concurrency the EmberJS universe ever produced, and which inspired 𝗥𝘅𝑓𝑥.
With concurrency, cancelation, animation, user feedback, and other best UX practices. CodeSandbox
Here we build an Alarm Clock (of a variety you may already know!) . Pushing a time/set button down is the request, and the responses are all the updates of hour or minute we get from the H or M keypress events.
Because 𝗥𝘅𝑓𝑥 ensures your services don't depend upon your view, you can port the same service to any UI framework, Web or Native, trivially. These ports of the Alarm Clock to major UI frameworks took under half an hour each to do:
- React Code Sandbox
- Angular Code Sandbox
- Svelte Code Sandbox
- Vue Code Sandbox