-
Notifications
You must be signed in to change notification settings - Fork 6
Bang Async
ngx-bang/async
provides better DX with asynchronous tasks like RxJS and Promises.
To support the argument of keeping
ngx-bang
simple,ngx-bang/async
was created to be dependent on RxJS to provide some DX improvements to users. The fact thatngx-bang/async
is a secondary-entry point means that it is optional.
Let's get back to our CounterComponent
and expand our state
.
If you're not familiar with
CounterComponent
, please check out ngx-bang Usages
Assuming we have a new requirement that is to move the "Seconds have passed since last count changed" to a property in our state
// the value of interval() needs to become our "state" value now
// instead of logging to the console
effect(this.state, ['count'], () => {
const sub = interval(1000)
.pipe(map(tick => tick + 1))
.subscribe((tick) => {
console.log(`It has been ${tick}s since the last time you changed "count"`);
});
return () => {
sub.unsubscribe();
}
});
Let's start by adjusting our CounterComponent
state to include secondsPassed
interface CounterState {
count: number;
incrementCount: number;
decrementCount: number;
secondsPassed: number;
}
@Component({
template: `
<ng-container *stateful="state; let snapshot">
<button (click)="onDecrement()">-</button>
<p>{{snapshot.count}}</p>
<button (click)="onIncrement()">+</button>
<p>You have clicked increment: {{snapshot.incrementCount}}</p>
<p>You have clicked decrement: {{snapshot.decrementCount}}</p>
<p>Seconds since last "count" changed": {{snapshot.secondsPassed}}s</p>
</ng-container>
`
})
export class CounterComponent implements OnInit {
state = state<CounterState>({
count: 0,
incrementCount: 0,
decrementCount: 0,
secondsPassed: 0
});
ngOnInit() {
// remove the effect. We'll replace it with something else
}
onIncrement() {
/* ... */
}
onDecrement() {
/* ... */
}
}
Now instead of effect
, we'll use asyncConnect
which is imported from ngx-bang/async
import { asyncConnect } from 'ngx-bang/async';
interface CounterState {
count: number;
incrementCount: number;
decrementCount: number;
secondsPassed: number;
}
@Component({
template: `
<!-- the template -->
`
})
export class CounterComponent implements OnInit {
state = state<CounterState>({
count: 0,
incrementCount: 0,
decrementCount: 0,
secondsPassed: 0
});
ngOnInit() {
asyncConnect(
// π connect to the Proxy
this.state,
// π for this key
'secondsPassed',
// π update with value from this stream
interval(1000).pipe(map(tick => tick + 1))
);
}
onIncrement() {
/* ... */
}
onDecrement() {
/* ... */
}
}
asyncConnect()
subscribes to the connector
(eg: interval()
) and will automatically clean up on Component's destroy as well. Everytime the connector
emits new value, state.secondsPassed
gets updated with that value.
If you save and go to the template, you'll see that secondsPassed
increments every second now.
However, we miss one part of the requirement: "when count changes". Right now, secondsPassed
just keeps incrementing every second and will not reset when we change count
. Let's fix that
import { asyncConnect } from 'ngx-bang/async';
interface CounterState {
count: number;
incrementCount: number;
decrementCount: number;
secondsPassed: number;
}
@Component({
template: `
<!-- the template -->
`
})
export class CounterComponent implements OnInit {
state = state<CounterState>({
count: 0,
incrementCount: 0,
decrementCount: 0,
secondsPassed: 0
});
ngOnInit() {
asyncConnect(
this.state,
'secondsPassed',
// π we can pass in a tuple [connector, deps]
[
interval(1000).pipe(map(tick => tick + 1)),
// π deps is an array of keys from "state"
// π so we can observe multiple different values from "state"
['count']
]
);
}
onIncrement() {
/* ... */
}
onDecrement() {
/* ... */
}
}
That's it! Now the interval
that is used to update secondsPassed
will be re-new whenever count
changes.
Invoke and clean up a side-effect.
// `interval()` will be subscribed upon invoking
// and will be unsubscribed when `stateProxy` is destroyed
asyncEffect(stateProxy, interval(1000), (tick) => {
console.log(tick);// 0 1 2 3 ...
});
Create a AsyncActionsProxy
to hold executable actions with their respective observables.
Considering the following case where you'd need to listen to trigger some effects based on some UI actions; like loadProduct
, createProduct
etc...
One approach is to create different Subject
and expose their next
method via a readonly
class member.
// before
export class ProductStore {
state = state({products: [] as Product[]});
private $loadProduct = new Subject<void>();
private $createProduct = new Subject<Product>();
/* a couple more for $updateProduct and $deleteProduct */
readonly loadProduct = this.$loadProduct.next.bind(this.$loadProduct);
readonly createProduct = this.$createProduct.next.bind(this.$createProduct);
/* a couple more for updateProduct and deleteProduct */
init() {
asyncEffect(this.state, this.$loadProduct.pipe(
switchMap(/*...*/)
));
asyncEffect(this.state, this.$createProduct.pipe(
mergeMap(/*...*/)
))
}
}
export class ProductComponent {
constructor(public productStore: ProductStore) {
}
ngOnInit() {
this.productStore.loadProduct();
}
onCreate(product: Product) {
this.productStore.createProduct(product);
}
}
With asyncActions
, you only need to provide an interface of the actions you need with the value type associated with each action. AsyncActionsProxy
will lazily create the Subject
under the hood as the properties are accessed. Nothing is initialized upon creation.
// after
export class ProductStore {
state = state({products: [] as Product[]});
// π a Proxy that is lazily creating (and returning) Subject as property is accessed.
// π Nothing is created upon initialization
actions = asyncActions<{
loadProduct: void,
createProduct: Product
/* a couple more for updateProduct and deleteProduct, as type */
}>();
init() {
asyncEffect(this.state, this.actions.loadProduct$.pipe(
switchMap(/*...*/)
));
asyncEffect(this.state, this.actions.createProduct$.pipe(
mergeMap(/*...*/)
))
}
}
export class ProductComponent {
constructor(public productStore: ProductStore) {
}
ngOnInit() {
this.productStore.actions.loadProduct();
}
onCreate(product: Product) {
this.productStore.actions.createProduct(product);
}
}