When should one start using reactive programming libraries like Rx
?
I’ve been asked this a fair amount of times over the years as a user
and contributor to bacon.js
. One
heuristic I like to give is to reach for these tools when you’re
feeling like the protagonist of Christopher Nolan’s movie
Memento. Let me explain:
If you’re not familiar with this brilliant movie, the story revolves around a character suffering from Anterograde amnesia, the inability of forming new memories. His life unfolds in repeated ten minute episodes roughly of the following form:
The titular mementos is the character’s method of accomplishing long-term goals. These take the form of notes and Polaroid photos that he keeps on his person. Really important information he tattoos on his body as to not risk losing them.
Surprisingly, some code we write tends to weigh us down with the same problems burdening our protagonist. In particular the event-handler pattern follows quite a similiar flow.
function somethingHappened(event) {
/* you wake up in an event handler */
/* inspect the current state */
/* decide what to do next */
/* modify state so that we can take appropriate actions
next time we "wake up" in another handler */
}
Failing to figure out what is going by looking at the current state can often be a source of bugs. This is amusingly illustrated in a famous scene of the movie.
Event handlers also have a number of other problems. The recently trendy Redux is a big improvement — handlers explicitly declare what state they operate on, and return the new, updated state.
function reducer(state, event) {
// ...
return newState;
}
This has a number of benefits, primarily in terms of composability, predictability and testability, and is a reason for Redux’s ubiquitousness, reaching far outside its original home in the React world. It does not however completely solve all our problems when it comes to state management.
What one will eventually encounter is the need to code functionality needing some kind of long-term planning — behaviour that lives across multiple moments in time. Writing this type of functionality without a “long-term memory” can turn out to be quite difficult.
Some simple examples are debouncing or throttling, making and retrying failing requests, coordinating long-running animations etc. Solving these problems with handlers we end up needing similar “mementos”, responsible only for tracking our intent between handler invokations.
An indication something is wrong may be that one has a lot of state
variables such as numberOfPendingRequests
, timeSinceLastInvoked
,
isCurrentlyLoading
, remainingRequestRetries
, activePromise
or
something like lastInputValue
denoting some old state value that we
wish to keep around in order to make some kind of decision later.
This type of state is never really something we are concerned with presenting to our users[1][2]. Nor are we concerned with passing them to components, or persisting them between sessions in case our app is closed and opened again. Instead it’s just internal book-keeping — tokens for coordinating what to do between moments not sharing any additional context. I like to call the excessive presence of this type of state variables Memento Spaghetti.
If we head to the home page of
bacon.js
we can find a classic example
of a problem handily solved with reactive streams in the Movie
Search section. We wish to provide an auto-completion menu to a user
searching for movies in a database. (There’s a live version on the
bacon.js homepage if you want to check it out).
// do not emit events when input is too short
const movieSearch = (partialInput) =>
partialInput.length < 3
? once([])
: fromPromise(retry(3, movieAPI, partialInput));
}
// user input, debounced and de-duplicated
const text = $('#input')
.asEventStream('keydown')
.map(ev => ev.target.value)
.skipDuplicates()
.debounce(300)
// the *latest* search result
const suggestions =
text.flatMapLatest(movieSearch)
.onValue(...)
.onError(...);
When implementing this functionality we must keep in mind that the
response time from making a request may vary, so searches made for old
input may happen to arrive after later searches. (Handled in
flatMapLatest
). User input must be debounced as to not flood our
server with requests, and should be de-duplicated. We would like to
retry requests three times in case it fails, but we do not want to
perform a retry if user has changed the search string as it will be
outdated anyway.
Trying to write the same functionality using the event handler pattern may look someting like this
function onUserInput(event) {
const partialInput = event.target.value;
if (partialInput.length < 3) {
return;
}
// debounce
const currentTime = getCurrentTime();
if (currentTime - this.lastTimeInvoked < 300) {
return;
}
// deduplicate
if (partialInput === this.lastInput) {
return;
}
this.lastInput = partialInput;
this.lastTimeInvoked = currentTime;
this.currentNumberOfRetries = 3;
let promise = this.currentPromise = (function search() {
return movieAPI(partialInput)
.then(results => {
// check that we're not outdated
if (promise !== this.currentPromise) {
return;
}
this.results = results;
}, error => {
// check we're not outdated
if (promise !== this.currentPromise) {
return;
}
if (this.currentNumberOfRetries > 0) {
this.currentNumberOfRetries--;
promise = search();
} else {
this.error = error;
}
});
})();
}
We can see that such code fails miserably to use modularity and needs
to store excessive amounts of non-user-related state, in addition to
being quite hairy to get right. (You may think that simple debounce
and throttle
functions, such as the ones provided in jQuery
could
do part of the job, but in this instance these will fail to take into
account that duplicates of the same input should not count towards the
debounce rate. In general they turn out to not be quite enough, as
they live in a world without an explicit notion of emitting events).
If you’re noticing this type of variables in your application state
you may have served yourself a portion of Memento Spaghetti, and
taking the time to learn Rx.js
, redux-sagas
or similiar may be due
to be put on your schedule.
This will help you restore your ability to form long term memories, and make it much easier to coordinate functionality across more than a single moment in time.