Edan Schwartz
Software Engineer

A Dead-Simple Todo List with RxJS

18 Sep 2015

I’ve recently been playing around with RxJS. If you’re not familiar with RxJS, I would suggest watching this talk by Jafar Husain about how Netflix used RxJS to build it’s new front-end.

I’m really interested in trying to wrap my head around RxJS, and functional programming in general, just because it’s so different than what I’m used to doing. I’ve read a ton of articles, and pored through the RxJS docs, but I had a really hard time implementing anything more than a simple snippet.

There is a TodoMVC example using RxJS, and I spent quite a bit of time trying to grok what was going on in there. The example Todo app just tries too hard to show off all the features, which makes it really tough to understand for a beginner.

After a many meeting between my head and the desk, I finally came up with a simple list view using RxJS and Cycle.js.

So in the interest of saving the brain cells of other developers, I present to you:

A Dead-Simple Todo List with RxJS

What is RxJS?

In short, RxJS is a library for working with asynchronous streams, which it calls “Observables”. Let’s, for example, take a look at a stream of click events in RxJS.

JS Bin on jsbin.com

NOTE: This is a live example using JS Bin. Click the ‘Output’ tab to test out the application.

What we’ve written is essentially an event-handler for a button click event. But the code looks a lot more like data-processing logic than event-handling logic. In fact, it can be quite helpful to think about RxJS observables as asynchronous arrays.

Cool, but how do you code an actual application?

I’m glad you asked, because that happens to be the topic of this very blog post!

Let’s start by taking a look at Cycle.js, a small application component for RxJS. Cycle.js will render a virtual-dom from a stream of application states.

Let’s try integrating Cycle.js with our button-click example.

JS Bin on jsbin.com

As you can see, the core business logic is similar to our first example. The main differences are in:

Let’s refactor

That main() function is a little long, and I’m seeing view stuff right next to state stuff, which I don’t really like. The cycle.js docs propose a Model-View-Intent pattern. Without going to much into it, I’ll show you how that might look with this code.

JS Bin on jsbin.com

Ah… much better, dontchya think?

The Todo List

The TodoMVC spec requires a lot of things. But because we’re making a dead simple todo list, I’m only going to require two things:

  1. A user can add an item to their todo list, by typing text into a textbox
  2. A user can remove an item from their list, by clicking a delete button next to the item.

We’ll also make the app ugly semantic, so we don’t have to worry about CSS or strange markup.

Requirement 1: A user can add an item to their todo list

If you look at our even-numbers button example from earlier, you’ll see we already have the bones for a user to add something to a list view. Let’s see if we can just replace those buttons with a text input.

JS Bin on jsbin.com

Pretty good, eh?

Requirement 2: A user can remove an item from their list

This is where things get a little tricky. So far, we’ve just been taking a steam of inputs and adding each item in the stream to a list view. We could visualize that like so

Inputs: "work"...               "eat"...                     "sleep"...

State:  {                       {                            {
          items: ['work']         items: ['work', 'eat']       items: ['work', 'eat', 'sleep']
        }                       }                            }

View:   <li>work</li>           <li>work</li>                <li>work</li>
                                <li>eat</li>                 <li>eat</li>
                                                             <li>sleep</li>

The problem is that now we want to remove an item from the list view, and there’s no way to go back and remove an item from the stream.

So instead of thinking about a stream of list items, let’s try thinking about a stream of operations on state. What do I mean by that? Well, let’s start by looking at an addTodo operation:

var addOperation = newItem =>
    state => ({
        items: state.items.concat(newItem)
    });

As you can see, the addOperation returns a function which receives a state, and sends back a modified state with the new todo item:

var state = { items: ['work', 'eat'] };

// Create an operation which adds 'sleep' to state.items.
var addSleep = addOperation('sleep');

addSleep(state);        // { items: ['work', 'eat', 'sleep'] }

And we could easily do this same thing for a removeTodo operation

var removeOperation = itemToRemove =>
    state => ({
        items: state.items.filter(item => item !== itemToRemove)
    });

var state = { items: ['work', 'eat', 'sleep'] };
var removeWork = removeOperation('work');

removeWork(state);      // { items: ['eat', 'sleep'] }

Makes sense? Yes? Good.

A stream of operations

So now, instead of thinking about working a stream of todo items, let’s think about working with a stream of operations on our state:

Inputs:     "work"                 "eat"

Delete Btn:                                                     "eat"

Operations: addOperation("work")   addOperation("eat")          removeOperation("eat")

State:     {                       {                            {
             items: ['work']         items: ['work', 'eat']       items: ['work']
           }                       }                            }

View       <li>work</li>           <li>work</li>                <li>work</li>
                                   <li>eat</li>

We’ll implement this pattern by mapping our intents to operations, and then applying each operation to the state using scan():

const Operations = {
  AddItem: newItem => state => ({
    items: state.items.concat(newItem)
  }),
  RemoveItem: itemToRemove => state => ({
    items: state.items.filter(item => item !== itemToRemove)
  })
};

function model(intents) {
    var addOperations$ = intents.addTodo.
        map(item => Operations.AddItem(item));

    var removeOperations$ = intents.removeTodo.
        map(item => Operations.RemoveItem(item);

    // Merge our operations into a single stream
    // of operations on state
    var allOperations$ = Rx.Observable.merge(addOperations$, removeOperations$);

    // Apply operations to the state
    var state$ = allOperations$.
        scan((state, operation) => operation(state), { items: [] });

    return state$;
}

Here’s the whole thing, in all it’s dead-simple glory.

JS Bin on jsbin.com

Let’s review what we did here:

I feel like this is a fairly elegant approach to building applications, and I hope this helps you get started with RxJS and Cycle.js.

If you want to play around with the Todo list app, you can download my repo on github. You can also see how I pulled out the item logic into a custom element.

I also put together a canvas pong game using Cycle.js with a custom Canvas driver. It’s a bit of a work-in-progress at the moment, but you’ll get an idea of how you can build a more complex application with RxJS.

Happy coding!

blog comments powered by Disqus