-
-
Save alecmce/791a7a1f2b8e365b7bb453abd244a392 to your computer and use it in GitHub Desktop.
import { Observable } from 'rxjs/Observable'; | |
import 'rxjs/add/observable/fromEvent'; | |
import 'rxjs/add/operator/filter'; | |
import 'rxjs/add/operator/map'; | |
import 'rxjs/add/operator/combineLatest'; | |
import 'rxjs/add/operator/distinctUntilChanged'; | |
import 'rxjs/add/operator/takeUntil'; | |
/** | |
* I am learning rxjs. The most complicated part is understanding the | |
* most elegant way to combine two upstream observables when they play | |
* different roles to the downstream behavior. Almost all examples of | |
* combining upstream observables imagine that the two observables are | |
* merged. | |
* | |
* In this example I imagine three actors: an active stream; a mousemove | |
* stream; and an update subscriber function. I want the update to be | |
* called with the mousemove event, only when active is true. | |
* | |
* There are two strategies offered, based on my very 2 day experience | |
* of rxjs. Neither seems quite right to me. I currently prefer the | |
* strategyUsingNestedSubscribers. Is there a 'best' way in rxjs? What is | |
* it? | |
*/ | |
const active = makeActive(); | |
const mousemove = makeMouseMove(); | |
type Update = (event: MouseEvent) => void; | |
function strategyUsingNestedSubscribers(update: Update) { | |
active | |
.filter((flag: boolean) => flag) | |
.subscribe(() => { | |
mousemove | |
.takeUntil(active) | |
.subscribe(update); | |
}) | |
} | |
function strategyUsingCombineLatest(update: Update) { | |
mousemove | |
.combineLatest(active) | |
.filter(([e, flag]: [MouseEvent, boolean]) => flag) | |
.map(([e, flag]: [MouseEvent, boolean]) => e) | |
.subscribe(update); | |
} | |
/** A reference example for how an active stream may be generated. */ | |
function makeActive(): Observable<boolean> { | |
return Observable.fromEvent(checkbox, 'change') | |
.map((e: Event) => checkbox.checked) | |
.distinctUntilChanged(); | |
} | |
/** A reference example for how a mousemove stream may be generated. */ | |
function makeMouseMove(): Observable<MouseEvent> { | |
return Observable.fromEvent(document, 'mousemove'); | |
} |
Thanks to both @staltz and @trxcllnt! Yes, I realized that the first strategy would be an antipattern because of the nesting, which is why I tried to come up with the second strategy. There are always many ways to shave a Yak! But, not all Yak-shavers are equal. Essentially strategyUsingCombineLatest was my clumsy attempt to write what you both did more elegantly with switchMap and withLatestFrom.
I agree that withLatestFrom is preferred to switchMap, because of the dependency relationship it expresses on mousemove and active. switchMap conceptually nests mousemove in a condition on active, even if it avoids explicit nesting the way my strategyUsingNestedSubscribers worked. It is in some ways imperative control flow, hiding in Yak's clothing.
@alecmce one last thing worth mentioning: the switchMap approach will remove the DOM listener for the mousemove event while active = false
(since it disposes the subscription to mousemove, and fromEvent
removes the DOM listener on disposal), but withLatestFrom won't. This distinction is subtle and usually either is fine, but keeping the subscription open can contribute jank in large-ish apps with many DOM listeners active.
@trxcllnt, thanks for pointing it out. Yeah, another thing to keep in mind!
@staltz comment is how I'd do it, but as with most problems in software, there's more than one way to shave this yak.
@alecmce your intuition is right in the first example, but the implementation is off a bit. Nesting
subscribe
calls is an anti-pattern in Rx, both because it's easy to mismanage subscriptions and because we have 5 core flattening strategies that will do it all for you :-). (They're namedmerge
,concat
,switch
exhaust
andexpand
.) If you wanted to go this route, you'd likely want to use theswitch
flattening strategy viaswitchMap
:The
switch
flattening strategy will always switch to the latest inner Observable. When theactive
Observable emits a new flag,switchMap
will unsubscribe from the current inner Observable it's flattening, and subscribe to the new one returned by the switchMap selector. Cheers!