Implementing a Functional-Reactive Search UI with Bacon.js

Most event handling code exists to do some work based the state of the system at that time. Usually we call out to other objects to make Ajax requests, UI updates, etc.

The problem is that these event handlers usually take some time to understand what each line is doing. Responding to an event usually involves at least two or three actions, yet we do it all in one function.

What if our code could make this clear by saying "when E happens, do X, then do Y and then Z"? This is the problem that Functional Reactive Programming (FRP) solves.

Event Streams

An Event Stream is a wrapper around an event with a similar publish/subscribe mechanism. Every event that happens in your system has some sort of value associated with it: DOM events have the Event object and Ajax events have a response. Event streams allow you to subscribe to different events and respond to them all in a uniform way.

Bacon.js provides a great JavaScript implementation of FRP and Event Streams, and (bonus!) it plays nicely with jQuery.

Binding to click/keyboard events

Our search is kicked off by a button click. We bind to the button's click event by using Bacon's built-in $.asEventStream. This will convert any jQuery event binding to an event stream. So we can bind to click using

    var clickStream = $("button").asEventStream("click");
    clickStream.onValue(function(){
        // onValue is the actual event binding
    });

But we also want the user to be able to press enter. Typically this would require something ugly like:

    $("input:text").bind("keyup", function(e){
        if(e.keycode == 13){
          doStuff();
        }
    });

Instead with Bacon, we can combine multiple streams and subscribe to all of them at once with:

    Bacon.mergeAll(clickStream, enterKeyStream).onValue(function(value){
        doStuff(value);
    });

Making an Ajax Request

If you need to do an extremely simple Ajax request and do nothing with the return value, you can do something like this:

    $("button").asEventStream("click").map(function(){
        return $("input:text").val();
    }).onValue(function(searchTerm){
        return $.getJSON("/search?q=" + searchTerm);
    });

Of course, this isn't extremely useful. Bacon.js has a few utility functions for creating Event Streams from asynchronous calls. In this case we can use the lovely Bacon.fromPromise, which takes a function that returns a promise and calls its subscribers every time that promise resolves. Since all of jQuery's ajax helpers return promises we can wrap our ajax call in a special function like:

    function ajax(query){
        return Bacon.fromPromise($.getJSON("/search?q=" + value));
    }

Then subscribe to it just like any other event stream:

    ajax("blah").onValue(function(data){
        renderResults(data); // do something with the response now
    });

Event Streams allow us to compose anything asynchronous; be it DOM events, custom events or Ajax requests. They are the great equalizer. How do we go from a stream of user interactions to an ajax request though? So far we've been combining mostly with .map, let's try it with our ajax function:

    clickStream.map(extractSearchTerm).map(ajax).onValue(function(value){
        // what is value?
    });

It turns out if we do it this way, value is just our ajax event stream, which means our subscriber would look like:

    clickStream.map(extractSearchTerm).map(ajax).onValue(function(value){
        value.onValue(function(value){
            renderResults(value);
        });
    });

This works, but YUCK! We're trying to get away from nested callbacks, right? If only there was a way to take the stream that gets returned from ajax() and automatically subscribe to it within the current stream. This is exactly what the flatMapLatest helper does. Using flatMapLatest we'd have:

    clickStream.map(extractSearchTerm).flatMapLatest(ajax).onValue(function(value){
        renderResults(value);
    });

Adding a Spinner

Usually adding an Ajax spinner means inserting something into the DOM at the start of an Ajax request, and removing it when the request completes. The simplest way to do that would be to show the spinner at the start of the stream and hide it at the end.

So far we've used map to combine our streams, but showing/hiding a spinner doesn't give us a value to pass on. Instead, we can use Bacon's doAction helper that takes a function, subscribes it to the current stream and then returns that stream. When the ajax call complets, it passes the response to the next subscriber in the chain. It looks something like:

stream.doAction(showSpinner).
    flatMapLatest(ajax).
    doAction(hideSpinner).
    onValue(...);

Putting it all together

I've included a jsbin at the end of this post that shows all of this put together in a working demo. This style is useful for more than just browser apps; I'm using ReactiveCocoa for the UI of my menu bar-based static site generator Staticly. Using FRP it's much easier to see exactly what's happening when, which removes a lot of the pain of maintenance.

What do you think? Does this sound useful? Is there more you'd like to see done in this style? Hit me up on Twitter, App.net or via email with your thoughts.

Published on by Joe Fiorini.