Monday, July 22, 2013

Local Reactivity in Meteor Web Applications

Every once in a while I stumble into something that changes the universe for me, allowing me rethink how I approach problems and develop solutions. Stumble because, while I can't necessarily plan on things like this happening, if I try to listen to people and keep my eyes open, I figure I'll eventually get lucky. When Meteor buzzed in a Cincinnati Javascript User Group meeting this Spring, I was happily surprised. And now just a few months later, it's become my new go-to web application framework.

Rather than try to introduce you to Meteor myself, I'll simply defer to the meteor web site and let them do the talking. The site includes a high-level description of the framework, detailed documentation, example code and links to videos. The quality of the description and documentation alone merit your attention - and the capability of Meteor deserves your respect. This stuff is brilliant.

Of all the things Meteor brings to the table, Reactivity is the game changer for me. Updates to data sources cause dependent computations to be re-run, and the application shows updates immediately. The burden of carefully choreographing a dance between the server and the client to display pleasing changes in a timely way has been lifted. The dance is already beautiful and the performers are well-rehearsed. I can focus on building great user experience rather than on things like scratching my head over problems with data structures or working the weird bugs out of ajax callbacks.

Recently, when developing a web app I found myself wanting to use this strategy locally as well, building reactivity to be used only within a client. Though Meteor gives you session-level reactivity that propagates changes made to session variables locally, this wasn't quite what I wanted. Session variables are global to the client; I wanted variables that were at least scoped so I could keep them more closely associated with the functions and objects that use them. I've been conditioned to believe that global variables smell bad and I'm not ready to give up that sense yet.

Instead, I opted to adapt a notion that was mentioned in the Meteor documentation. I associated a variable and a dependency in an object and called it an injective. The idea is that I can inject client values into the reactive flow. In Coffeescript,

    Deps.injective = (init, options) ->
        _value: init ? 0
        _dep: new Deps.Dependency
        _force: !!(options && options.force) ? false
        set: (value) ->
            if (@_value != value) || @_force
                @_value = value
        get: ->
        depend: ->
        changed: ->
        force: (f) ->
            @_force = !!f

The internal _value and _dep are managed through the get and set functions. Calling get returns the value while making the function asking for it dependent on the injective. Updating a value using set re-computes all functions that depend upon it.

A quick example perhaps, again in Coffeescript.

    App.innerWidth = Deps.injective window.innerWidth
    $(window).resize -> App.innerWidth.set window.innerWidth

This code creates an injective that tracks the inner width of the browser window. When the window resizes, the injective's value is set to the new inner width. While this might not sound like much, anything that has been computed based on the getting the value of innerwidth will be automatically recomputed when it changes. In the context of this example, that means that you don't have to know about the 23 layout decisions you made based on this value and how they'll need to propagate, and then manage it all yourself. Instead, you just grab the corner of the window and resize it, and watch the magic happen.

The depend and changed functions provide access to the dependency mechanism and are used by get and set. When depend is called, the injective adds the calling function to its list of dependents. An update to the injective using set will re-run these dependent functions. The changed function is what triggers the dependents to be re-run.

Finally, force is a way to force changes to objects and arrays values to be propagated without having to write any special comparitors. When the value being set is a scalar, == works: if the value is different it is copied and changed is called. If the value is an object (or an array) and the change is made within the object itself, == won't report the object has changed; a more complex comparison must be written to do the test. But that really isn't needed if you know about the complexity already. The force function (or constructor argument) provides a simple shortcut: if _force has been set to true then the set function trusts that the incoming value is different; set just goes ahead and updates the value and propagates the change. If something more complex is needed, values in the object can be adjusted and the injective's changed function can be called directly.

I've submitted injective (a Javascript version) to the meteor github repository. Maybe they'll integrate it, maybe not; I wasn't fixing a bug, just adding an enhancement so it doesn't have any sort of priority. However, it's a clean way to inject values into the reactivity stream that's useful for me. If it looks like it may be useful for you too, then enjoy!

No comments: