Memento spaghetti

Posted on June 28, 2017

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:

  • “Wake up” in an unfamiliar situation, with no memory of how he got there.
  • Inspect the state of world around him in order to figure out what is going on in his immediate surroundings.
  • Decide on what actions to take.
  • Produce “mementos” to guide his future actions as he inevitably wakes up in a similar situation.

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.


  1. Something like isCurrentlyLoading may be used to present something like a load spinner and is not always bad practice. ↩︎

  2. Note that storing an old state value like lastInputValue in your application state is different from having access to it "naturally" as a reducer argument. ↩︎