Understanding Reactivity

Introduction

If someone were to ask me just what makes Meteor so special, I would answer with one word: reactivity. While Meteor has lots of nice features— simplified async, build tools, the ability to build mobile apps— the one thing that completely sets it apart from, and therefore improves upon, most other web frameworks is the power of reactivity. The ability to receive real-time updates from the server and low-bandwidth client-server communications is not only a boon to web application developers, but to their clients as well.

This comes at a cost, and for developers, that cost is cognitive. To understand how reactivity works, be prepared to rethink how you approach client/server communication.

How Most Frameworks Work

Most web frameworks use HTTP as a way to communicate between client and server. The exchange looks something like this:

client: Hi website, I'd like to visit.  
server: Welcome! Here's some HTML, CSS, and JavaScript for you.  
client: Great! Let me display that data to the user.  
[a few moments later]
client: Hey, has that data changed?  
server: Nope.  
client: Okay, how about now?  
server: Still, no.  
client: And now?  
server: Yeah, here's the new data.  
client: Great, thanks.  
client: Any new data?  
server: No.  

In the above exchange, only the client is able to initiate communication. When it was typical to send static HTML to browsers, this made total sense. Who needed the ability to send web pages to clients who didn't request them? However, now that you have sophisticated web and mobile clients communicating with your server, the nature of this exchange is wasteful and cumbersome.

The equivalent Meteor exchange might look something like this:

client: Hi website, I'd like to visit.  
server: Welcome! Here's some HTML, CSS, and Javascript for you.  
client: Great, I'd also like to know what's up with Accounts and Orders.  
server: Okay, hold on. Here you go.  
[a few moments later]
server: Oh hey, a new account was added. Here it is.  
server: And also, six new orders. Here they are.  

You can see how much more efficient and quick that is. It also means that your app is acting in real time, so when the data changes on the server, it's also reflected on the client.

With other frameworks, polling always requires a trade-off between speed of update and efficiency. The more often you poll, the less efficient you are. On the other hand, you're also able to react to data changes quicker and vice versa. Meteor requires no such trade-off.

Sidebar: Why is HTTP a terrible protocol for web applications?

The primary reason? HTTP is "stateless." Essentially, every request is treated as if it's coming from a new stranger with no context. If a resource requires authentification credentials to be sent, the server must parse the entire request again, which contains a lot of overhead. It's like having to reintroduce yourself to a friend every time you wanted to have a conversation. It's noisy and inefficient, and it may mean that the content of what you're saying is buried under all that extra data.

Connections in Meteor work differently: once established, they are persistent. The server remembers what transpired with that connection, much like a real-life interaction.

How Meteor Works

While Meteor does use HTTP for sending HTML, CSS, and JavaScript (and for file uploading), most client-server communication occurs over DDP, which stands for Distributed Data Protocol.

What is DDP?

The way I describe it, it might sound like DDP is a network protocol, but it's not. DDP is basically JSON over WebSockets (Meteor actually uses SockJS for fallback where the client doesn't support WebSockets, but WebSockets are pretty well-supported across modern browsers, so we're going to gloss right over that). DDP allows you to send JS objects back and forth, utilizing a small set of commands (connect, subscribe, and method) to send data back in response to these commands (either data and/or the results of the command). You can see the full spec here, but it's pretty simple.

The nice part is that when you are using Meteor, you never actually need to worry about using DDP. It just happens quietly in the background, whether you're subscribing to publications or executing methods. Still, it will probably help you better understand what's happening in your application if you can follow along with the DDP requests. You can use some Meteor-specific Chrome plugins to help with that, or simply use the Network tab in the Chrome Developer tools.

What are WebSockets?

WebSockets is the name of a WC3-adopted protocol for establishing full-duplex TCP connections between client and server on port 80/443. Unlike HTTP, beyond the initial connection request (delivered as an upgrade request, similar to how clients can request to be upgraded to SSL), the WebSockets spec does not define the content of messages between client and server allowing any sort of data to be transferred. Meteor then uses DDP to define the shape (JSON) and content of messages. WebSockets are elegant in their simplicity and I think you will see more and more applications utilizing them in the near future.

Thinking Reactively

So now that you have a better understanding of how Meteor implements reactivity, let's see how you can use it in your applications.

In Blaze

At its simplest, reactivity can be a little bit of "free magic." You have data, you display it, it changes, and the changes are displayed. An example of this is the "Hello World" application that gets created when you run meteor create.

We see the counter variable displayed here:

<template name="hello">  
  <button>Click Me</button>
  <p>You've pressed the button {{counter}} times.</p>
</template>  

We also see the code which creates the variable on the instance of the template and updates it when the "Click Me" button is clicked.

Template.hello.onCreated(function helloOnCreated() {  
  // counter starts at 0
  this.counter = new ReactiveVar(0);
});

Template.hello.helpers({  
  counter() {
    return Template.instance().counter.get();
  },
});

Template.hello.events({  
  'click button'(event, instance) {
    // increment the counter when button is clicked
    instance.counter.set(instance.counter.get() + 1);
  },
});

However, what you don't see here is any code to find that element in the DOM and update it. Meteor just does that for you automatically.

This is all well and good when the data is being changed on the client-side and being displaying directly. But what about when that data changes on the server and you want to perform some sort of operation on that data? This is where subscriptions and Tracker.autorun come into play.

When you create a subscription, you are doing two things:

  1. Fetching the current data that is being published.
  2. Telling the server that you want to be notified of any changes and to receive the contents of those changes.

Tracker.autorun allows us to define functions that should be performed whenever the data we are watching changes.

Let's assume that we've subscribed to a publication called "Orders," and we want to display the total number of orders. The code might look something like this:

Template.something.onCreated(function () {  
  this.totalOrders = new ReactiveVar(0);
  this.orders = this.subscribe("Orders");
});

Template.something.onRendered(function () {  
  // Do reactive stuff when subscribe is ready
  this.autorun(function () {
    if (this.orders.ready()) {
        const totalOrders = Orders.find({}).count();
        this.totalOrders.set(totalOrders);
      }
  });
});

We could go much deeper here, but the pattern is basically this: subscribe to data, then do something when that data changes (after the subscription is ready).

In React

Handling reactivity in React is even easier. React is already designed to respond to changing data so we don't need to wedge another paradigm into React to make it reactive. We can just use "containers."

In Reaction, we use react-komposer to create data containers. Data containers can be created at whatever level it makes sense to respond to changing data. A simple example might look like this, which is a snippet of example code found here:

import React, { Component, PropTypes } from "react";  
import ReactDOM from "react-dom";  
import Task from "./Task";  
import { Tasks } from "/imports/api/tasks";  
import { composeWithTracker } from "/imports/api/komposer";

class TaskContainer extends Component {

  handleSubmit(event) {
    event.preventDefault();
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();
    Tasks.insert({ text, createdAt: new Date() });
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input type="text" ref="textInput" placeholder="Type to add new tasks"/>
          </form>
        </header>

        <ul>
          {this.props.tasks.map((task) => {
            return (<Task key={task._id} task={task}/>)
          })}
        </ul>
      </div>
    );
  }
}

TaskContainer.propTypes = {  
  tasks: PropTypes.arrayOf(PropTypes.object)
};

function composer(props, onData) {  
  if (Meteor.subscribe("Tasks").ready()) {
    const tasks = Tasks.find().fetch();
    onData(null, { tasks });
  }
}

export default composeWithTracker(composer)(TaskContainer);  

The repo contains a version of the standard "ToDo" app from the Meteor tutorial, rewritten to use react-komposer. You can see how setting up your data and just passing it into the onData method makes it simple to reason about what data you are using and where it is coming from. That data is then available as props to the component.

Check out the docs for react-komposer. You'll see that Komposer gives you even more options, such as only watching and reacting to a certain subset of the props being passed in.

DDP Outside of Meteor

DDP's biggest obstacle may be the fact that it was created for Meteor. Because the protocol hasn't been widely adopted by other frameworks, you would be limited to only using a Meteor client. However, while DDP is not widely adopted, WebSockets are well-known. If you look at the documentation for DDP, you can also tell that implementation is pretty straightforward.

This had lead to quite a few DDP client implementations for other languages and platforms, such as this driver for iOS/Swift. You can check out a more complete list here. Even non-Meteor clients can benefit from the power and efficiency of DDP.

Since Meteor itself is written in Node, of course there's a Node DDP client. Below you'll find an example of how simple it is to connect to Meteor. If you're running a local version of Reaction, this simple script will subscribe to the "Products" collection and return all the products (you will need to install the ddp and lodash libraries for this snippet to work). And while this example points to your local instance, you may also be pointing to any Meteor server, or even multiple Meteor servers in a microservice environment.

var _ = require("lodash");

var ddpclient = new DDPClient({  
    host: "localhost",
    port: 3000,
    ssl: false,
    autoReconnect: true,
    maintainCollections: true
});

ddpclient.connect(function (error, wasReconnect) {  
    if (error) {
        console.log("DDP Connection error");
        return;
    }

    if (wasReconnect) {
        console.log("connection reestablished");
    }
    console.log("DDP connected");


    ddpclient.subscribe("Products", [], function () {
        console.log("Products fetched");
        var products = ddpclient.collections.Products;
        _.map(products, function (product) {
            console.log("==> " + product.title);
            console.log("==> " + product.price.range);
        });
        ddpclient.close();
    });

});

ddpclient.on("message", function (msg) {  
    console.log("DDP message" + msg);
});

The ddpclient.on("message") event handler will show you all the messages that the client is receiving so you can see how the client and server exchange messages. For more info, check out the DDP client docs.

Final Thoughts

I hope this article has helped you understand Meteor's built-in power through reactivity, as well as some of the different ways to think about web development when using a Reactive model.

If you have questions or comments you can reach me on the Gitter chat channel or at brent@reactioncommerce.com

comments powered by Disqus