Last fall, after having worked with React or similar technologies for a couple years, I took a contract working with Marionette.js. A much older technology, based on Backbone.js, it was uncomfortably familiar. I was dealing with problems I hadn’t had to think about for a while! But there were some benefits to my contact with new technologies…
Model View Controller (MVC) is the old standard. It’s been a darling of the architecturally pure for years. A crisp separation between an application’s data (Models), user interface (Views), and business logic (Controllers). Rails was held up as true MVC architecture. But it wasn’t always so crisp. You’d have so much business logic that it didn’t feel right in the Models, or it wasn’t clear whether a given bit of code should go in the Controller or the Model.
The client side largely wasn’t making these architectural distinctions until 2010, when Backbone was first released. It was a great step forward for front-end development, arriving in a world of mostly spaghetti-style pure jQuery apps. But Backbone wasn’t perfect. Its Views were really ViewControllers, Models had a reference back to their View, and its Events system could and often did tie everything to everything else.
Meanwhile, in 2013 Facebook releases its groundbreaking React library. As part of the announcement, it talks about a new way to think about application architecture. Instead of MVC, they suggested their Flux pattern. Instead of Models you just have plain data with no behavior bundled with it. Instead of Controllers, you have plain application logic. You’ve still got Views, now called React Components.
Compared to Backbone, the differences were especially stark. Instead of events flying everywhere, potentially triggering further events, Flux demanded that events flow in just one direction. With its dispatcher in place, one event could not trigger another event. These constraints made applications so much easier to understand!
How reconcile these two? Take what I had learned with Flux and apply it to Marionette!
My very first task on the contract was a refactoring to make the app more predictable, while also helping me to understand it more quickly. The idea was to push the application towards a Flux-inspired structure. We’d still have Backbone Models, but we’d reduce the number of places where they were modified.
Every page of the Single Page Web App (SPA) was made up of a top-level Marionette
LayoutView. On load, it created the necessary Backbone Models and Collections, passed that data into its child Views, then kicked off the initial data load from the server. Based on user interaction, child Views would update the UI, modify those Models and Collections, kick off a sync with the server, and when the server response came in, Views across the app would re-render based on the new data.
So I began the long process of finding all places where Models or Collections were mutated. Child View code became a
trigger() call that needed to propagate an event all the way to the top level
LayoutView. This meant a lot of intermediate
trigger() calls up the view tree.
In the course of the refactoring, I discovered several lurking bugs involved in cascading data changes. One model would be updated, then based on the result of some logic, another model would be updated and the server would be notified, finally resulting in another model update. Every step was in a different part of the view tree, and every model was being listened to by various views.
After my refactoring, every step was in the same top-level
LayoutView, making it far easier to understand and debug the asynchronous flow. I fixed a few bugs which were now easy to see. But it still wasn’t a clean design, from my perspective. The state space in that asynchronous flow had too many invalid states.
Given that we controlled the API, we could easily get all the data we needed at once, shrinking the client-side state space. But Backbone’s automatic REST wireup made us think in very small units, each one requiring its own server endpoint. That then forced the client into flowing data from one Model or Collection to the next.
Backbone had reduced design creativity. The application was stuck in the Backbone Box!
When I was tasked with adding a new ‘recent searches’ feature, I was sure to avoid the Backbone Box. I did use a Backbone Model to store the data and interact with the server, but the structure of its data wasn’t a single conceptual ‘model,’ a set of simple key/value pairs. It was two arrays, each containing full objects.
This was a self-contained system, no involvement from a
LayoutView. The only mutation to the
RecentSearches model was in its own
addSearch() method. A new search would be sent to the new Express endpoint I added, and the entire new set of recent searches would come back from the API. One server request per user search, and every time the local data was replaced with the response. Any subscribed View would re-render.
Simple and easy. No Collection necessary. Easy to reason about. Not a perfectly normalized Model, but well-suited to the scenario.
I went further when we introduced an entirely new page to the SPA. Thinking outside the Backbone Box, I did away with Backbone Models when writing some key new UI. Since roundtrips with the server weren’t happening in this scenario, I didn’t need to lean on Backbone REST behaviors.
With hard-to-trace event wireup no longer a temptation, it was very easy to reason through what was happening to the data. It was clear which events modified things. And the external interface was clear: data in, and data out. Easy. Later we would pack data into a collection to be rendered by a Marionette
It was very clean, and it was very Flux-inspired.
Even with all of our progress, I still wasn’t comfortable. There are three major areas I really don’t like about Marionette.
First is the cliff moving from a simple
ItemView to a more complex set of subviews managed by a parent
LayoutView, a refactor which involves large logic changes and two new files for each new view. That high cost incentivizes large Views.
Second is the temptation to make little tweaks to the UI with jQuery. In theory, every change to the UI should be a template rendered with data. But it’s so much easier to make a tiny change, since jQuery is already there, bundled with Backbone. The temptation is especially strong in a
LayoutView because you can’t actually re-render it. That would blow away all of its child views!
Third is the standard state management problem, exacerbated by holdover jQuery/Bootstrap techniques. At one point we installed a Bootstrap collapsible section on a page with a chart rendered by Chart.js. It was supposed to be a simple drop-in change, but if a chart rendered while in a collapsed section, it would render with zero height and be gone when the section was expanded again. Thus, I had to hook into the jQuery/Bootstrap events related to showing a collapsed section, and force re-render of the chart (without animation).
None of these problems exist in the world of React and Flux. The team discovered this when I designed and delivered React training to them a few months later.
There’s been a lot of complaining about the high churn in the front-end development world. But I believe that we are advancing the state of the art. New techniques and libraries aren’t just about the flashiness. They really are innovating, and those innovations can be applied to legacy technology.
And that means application development in the future will be much more focused on user value instead of wrestling with the technology. I’m excited.