Skip to content

Instantly share code, notes, and snippets.

@alecmce
Last active October 11, 2017 19:00
Show Gist options
  • Save alecmce/791a7a1f2b8e365b7bb453abd244a392 to your computer and use it in GitHub Desktop.
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');
}
@staltz
Copy link

staltz commented Jul 28, 2017

This is why withLatestFrom was created

mousemove
    .withLatestFrom(active, ([e, flag]) => flag ? e : null)
    .filter(e => e !== null)
    .subscribe(update);

@trxcllnt
Copy link

@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 named merge, concat, switch exhaust and expand.) If you wanted to go this route, you'd likely want to use the switch flattening strategy via switchMap:

function strategyUsingNestedSubscribers(update: Update) {
  active
    .switchMap((flag: boolean) => flag ? mousemove : Observable.empty())
    .subscribe(update);
}

The switch flattening strategy will always switch to the latest inner Observable. When the active 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!

@alecmce
Copy link
Author

alecmce commented Jul 29, 2017

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.

@trxcllnt
Copy link

trxcllnt commented Jul 29, 2017

@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.

@alecmce
Copy link
Author

alecmce commented Jul 29, 2017

@trxcllnt, thanks for pointing it out. Yeah, another thing to keep in mind!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment