Key Takeaways
- Libraries for functional reactive programming a very powerful
- However, choosing the right operators to use can be overwhelming
- Luckily, getting to know just a few is not that hard and will get you a long way
- A good start is to learn about `map`, `filter`, `debounceTime`, `startWith`, `scan`, `merge` and `combineLatest`
- When combined, these operators really start to shine
Reactive programming techniques are becoming more prevalent in the constantly changing JavaScript landscape. This article series hopes to provide a snapshot of where we're at, sharing multiple techniques; variations on a theme. From new languages like Elm to the way Angular 2 has adopted RxJS, there's something for every developer, no matter what they're working with.
This InfoQ article is part of the series "Reactive JavaScript". You can subscribe to receive notifications via RSS.
You might have heard about RxJS by now. You might even already know how it works, why it's awesome, or you might simply be using them because your framework does. That's all nice and well, but the list of available operators can be quite overwhelming. If you're just getting started, which ones do you actually need?
In this article I'll list a few of the ones you'll most likely be using, along with examples that will help you get a feel for when each of these operators could come in handy.
A word about operators
When working with RxJS, everything revolves around the Observable. There are many ways to talk about Observables, but the simplest (although somewhat hand-wavy) way is probably to consider them as promises that can return multiple values. In this post, I'll assume that you already have an Observable available, e.g. given to you by your framework. If not, they're rather trivial to create from Events, Promises or even from plain Arrays.
Operators can use these existing Observables to create new Observables with values based on the existing one(s). We will indicate Observables by appending a $, e.g. click$ for an Observable containing click events.
Note that this introduction references RxJS version 5 specifically, but that equivalent operators are available for alternative reactive libraries such as Most.js, xstream, Kefir.js and Bacon.js.
Diagrams are courtesy of RxMarbles.com by André Staltz.
map
When to use
map is useful when the values in an Observable are useful to you, but are not in the format you want it to. For example, if you had an Observable representing click events, you could use map to create a new Observable that only represents the x and y coordinates of those clicks. Or perhaps you have an Observable that produces several floating point numbers, but you're only interested in rounded numbers.
How to use
It's basically analogous to the map method on Arrays: you pass it a function to transform each value, and it will return a new Observable with the transformed values:
const newObservable$ = $observable.map(callback);
Examples
Transforming click events to maps of coordinates:
const coordinates$ = clickEvents$.map(event => { x: event.clientX, y: event.clientY });
Rounding floating point numbers:
const roundedNumber$ = decimal$.map(Math.round);
filter
When to use
filter is useful when you're not interested in all the values produced by an Observable. For example, if you've got an Observable producing the current value of an <input> field whenever the user changes it, you might only want to act on that when the user's input is above a certain length (e.g. to make sure you only start searching when you can provide relevant results).
How to use
This operator is basically analoguous to the filter method on Arrays: you pass it a callback function that returns either true or false for a given value, and it will return an Observable producing just the values of the original Observable for which the callback returns true:
const newObservable$ = $observable.filter(callback);
Examples
Only produce input values of length three or more:
const searchTerm$ = inputValue$.filter(inputValue => inputValue.length > 2);
When to use
debounceTime is useful when you don't want too many values in a short amount of time. For example, when you have an Observable producing user input upon which you want to perform HTTP requests, it would be wasteful to perform lots of requests while the user is still typing. After all, there might not even be enough time for the user to actually see the results of those calls! With debounceTime, you can limit the number of values produced to at most one every given amount of milliseconds.
How to use
You simply pass it the minimum amount of milliseconds you want to have elapse between consecutive values. The resulting Observable will then wait at least that time after its last value before producing the latest value produced in that time by the original Observable:
const newObservable = $observable.debounceTime(milliseconds);
Examples
Only produce new search terms at most every 200 milliseconds:
const debouncedSearchTerm$ = searchTerm$.debounceTime(200);
startWith
When to use
The startWith operator is mostly useful when you have an Observable of values that might arrive over time, but for which you'd also like to set an initial value. For example, you might want to display user input, and a placeholder when the user has not yet provided any input. Or you could have a configuration option that the user can toggle by pressing a button, and which should be initialised to either true or false in lieu of the user having pressed the button.
How to use
Simply pass the value you want the new Observable to emit at first. After that, it will produce every new value produced by the initial Observable:
const newObservable$ = $observable.startWith(initialValue);
Examples
Display a placeholder when the user has not entered a username yet:
const username$ = usernameInput$.startWith('Please enter a username');
Provide an initial value for a binary configuration setting:
const nightMode$ = switchIsOn$.startWith(false);
scan
When to use
When you want to combine new values with old ones, scan is what you need. For example, this could be useful if the user earns points for certain actions, and you want to keep track of their current score. Another example is if you have paginated search results, and you want to append new pages to the previous ones when their HTTP responses come in.
How to use
It works analogously to the reduce method on Arrays: pass it a callback function that combines new values with an accumulator, and it will produce a new Observable that will produce values combined using that callback.
const newObservable$ = observable$.scan(callback);
Examples
Keeping track of a user's score:
const score$ = points$.scan((score, points) => score + points);
Appending new paginated responses:
const searchResults$ = apiResponse$.scan((searchResults, nextPage) => searchResults.concat(nextPage));
merge
When to use
merge actually works on multiple Observables. As its name implies, it merges the original Observables together, creating a new Observable that produces values whenever either of the original Observables does. An example usecase is being able to skip through a playlist using either Next or Previous buttons, with each button having an associated Observable producing representing click events.
How to use
Simply pass it the Observables you want to merge with the first one:
const merged$ = first$.merge(second$, third$, ...);
Note that since first$
is not necessarily more special than second$
and third$
, I usually prefer using merge as a standalone operator and passing all the Observables that I want to merge as arguments:
const merged$ = Rx.Observable.merge(first$, second$, third$, ...);
Examples
Controlling a playlist using two Observables:
const changeSongClick$ = Rx.Observable.merge(previousSongClick$, nextSongClick$);
combineLatest
When to use
combineLatest is useful when you have multiple Observables whose values only make sense when used together. For example, you might want to combine the responses of several microservices which come in asynchronously. Or you might have Observables representing user input in several form fields, which you want to combine into a single result.
How to use
Pass it the Observables whose values you want to combine with the first one, and a function that takes all those values and returns a combined value:
const combined$ = first$.combineLatest(second$, third$, ..., callback);
Again, you might want to use this operator standalone:
const combined$ = Rx.Observable.combineLatest(first$, second$, ..., callback);
Examples
Combining several HTTP responses to display a user's activity:
const profile$ = Rx.Observable.combineLatest(
userData$,
articles$,
comments$,
([userData, articles, comments] => ({ userData, articles, comments }))
);
Combining several dynamically updated form inputs:
const fullName$ = Rx.Observable.combineLatest(
firstName$,
lastName$,
(firstName, lastName) => `${firstName} ${lastName}`
);
Putting it all together
You've seen examples of many operators and when to use them. Each of them operated on one or more initial Observables and resulted in a new one. The result of this is that you are able to chain the operators together, at which point the real power of Observables starts to shine. Thus, let's look at a use case where we will use multiple operators sequentially to achieve a desired result.
Let's say we have a user-configurable number that can be incremented or decremented using their respective buttons. The desired end result is an Observable that emits the new value of that number every time the user presses one of those buttons.
We have two Observables -one for each button- that emit values whenever their respective buttons are clicked:
// incrememtButton and decrementButton are references to HTMLElements,
// e.g. obtained through document.querySelector
const incrementClick$ = Rx.Observable.fromEvent(incrementButton, 'click');
const decrementClick$ = Rx.Observable.fromEvent(decrementButton, 'click');
We don't care about the actual event data; all we care about is that the number is incremented by 1 when the increment button is clicked, and decremented by 1 when the decrement button is clicked. So let's map it to those values:
const increment$ = incrementClick$.map(event => 1);
const decrement$ = decrementClick$.map(event => -1);
Now, these two separate Observables are not that useful yet. Rather, what would be useful is an Observable producing the changes we want to perform to our number whenever one of the buttons is clicked:
const numberChange$ = Rx.Observable.merge(increment$, decrement$);
And this is where the magic happens: we can apply this change to whatever the current value of the number is, to get the new value whenever one of the buttons is clicked. Which will, of course, only work if the number is set to an initial value when no button has been pressed yet; we'll set it to 0:
const number$ = numberChange$
.startWith(0)
.scan((previousNumber, change) => previousNumber + change);
And that's all there is to it! Of course, we don't need all those intermediate variables. Try and see if you can follow the complete code, and do not hesitate to look up the definitions of the operators again above:
const number =
Rx.Observable.merge(
incrementClick$.map(event => 1),
decrementClick$.map(event => -1)
)
.startWith(0)
.scan((previousNumber, change) => previousNumber + change);
...and now consider what the above would look like without using Observables.
Conclusion
With the examples above, you should have a feel for the operators you need to get started with Observables, and when they're useful. You've seen that they can greatly ease working with asynchronous data, which is more often than you'd think. We've seen the example above of responding to user input in real-time, but hopefully you will now also be able to figure out e.g. how to pick relevant data from several microservice responses and combine them together.
That said, the purpose of this introduction was not to be comprehensive, so you will run into edge cases and caveats that will not be covered here. Luckily, pretty much everything that you will want to do is possible, and the RxJS website contains a handy tool to help you find the right operator. And if this article got you interested, I regularly write about RxJS and reactive programming at VincentTunru.com.
About the Author
Vincent Tunru is a front-end developer from the Netherlands. He's always looking to learn things that will broaden his thinking, and believes one the best ways to do so is by attempting to explain them.
Reactive programming techniques are becoming more prevalent in the constantly changing JavaScript landscape. This article series hopes to provide a snapshot of where we're at, sharing multiple techniques; variations on a theme. From new languages like Elm to the way Angular 2 has adopted RxJS, there's something for every developer, no matter what they're working with.
This InfoQ article is part of the series "Reactive JavaScript". You can subscribe to receive notifications via RSS.