We have been writing React at Gusto for over a year now. Our development teams are building all new features using React while simultaneously converting existing Backbone views as updates are needed. Along with this, we've done a complete overhaul of our frontend system, abandoning CoffeeScript in favor of ES6/7, using static analysis tools like ESLint heavily, utilizing our own flavor of the Flux architecture, and adopting build tools like Webpack to ease the burden on the Rails asset pipeline.
This will be the first entry in a multipart series about building user experiences at Gusto, detailing (1) our migration from Backbone to React and Flux and (2) our move from the Rails Asset Pipeline to Webpack.
In our first three installments, we'll be focusing specifically on transitioning from Backbone to React, breaking our experience down into three sections: (1) How we used to build UI with Backbone (and the ways it was deficient for us), (2) why we decided to migrate to React, and (3) a tutorial giving an inside look at how we write React today.
Why Our Stack Used to Make Sense
When development on the Gusto product began in January 2011, we chose the best tools available to us at the time: opting for a JS stack consisting of CoffeeScript, Backbone.js, and Handlebars, all served by the Rails Asset Pipeline.
Backbone.js was all the rage just a few years back and provided everything we needed for a thick, well modularized client-side app -- everything from the API layer to populate the data model all the way to client side routing and events.
We tested our code using Mocha, Chai, and Sinon, and utilized Teaspoon as our Rails-based test harness -- allowing us integrate our frontend specs with our Rails-generated JSON fixtures.
Ultimately, we were able write well-tested frontend code and abstract away our most common UI paradigms using inheritance, leaving us with a system that, at the time, felt productive and robust. These tools served us well for many years, enabling us to build rich user experiences in true single-page fashion. But as our team began to grow and the prospect of building UI-heavy systems like workers compensation and health insurance around the corner, we started to want more from our frontend architecture.
Breaking up with Backbone
We first felt the limitations of our frontend system when we were ramping up our state expansion efforts and pushing to be a nationwide payroll provider. Every state's setup is relatively similar in nature, but each one has a variety of specific UI needs.
In order to build the 50 states in an efficient manner, we had our backend State Builder DSL dynamically drive the frontend views. This was the first time we were trying to implement powerful patterns that extended well beyond the common use cases. When building a proof of concept in Backbone, we found it was painful for a variety of reasons:
Backbone is notorious for leaving references to "zombie views" around due to the lack of a built-in mechanism for cleaning up event handlers, so libraries like Coccyx became necessary to help deal with these issues.
In addition to needing to be diligent about cleaning up our subviews, we found that occasionally our top level views looked something like this:
class ShowView extends Backbone.View template: HandlebarsTemplates['employees/show'] className: 'employee-show-view' initialize: (options) -> @company = options.company ... appendSection: (viewClass, options) -> view = new viewClass(_(model: @model).extend(options)) @registerSubView(view) @$('#employee-sections').append(view.render().el) view render: -> @tearDownSubViews() @$el.html(@template(employee: @model.toJSON())) @appendSection(AddressesView) @appendSection(SplittableShowView) @appendSection(CompensationsView, company: @company) @appendSection(FederalTaxesView) @appendSection(SpecialExemptionView, company: @company) @appendStateTaxViews() @appendDismissView() @appendRehireView() @appendFormsView() @appendPaystubView()
Just to determine what our view would look like, we had to reference both the template and the view to see where on the DOM the subview was being mounted and how it was being attached. Ideally, we want to be able to take a quick scan of a file and understand exactly how it was using subcomponents, as well as what it looks like when rendered.
Events Triggering Manual DOM Updates
A common pattern in Backbone is to listen to a variety of DOM events (click, change, etc) and when fired, manually update the DOM using jQuery to hide and show different elements.
As a payroll system, we often deal with complex data entry, and we capture this information using heavily interactive forms. One example of this is our payment method form, which asks for different things based on whether you need to be paid by check or by direct deposit.
class PaymentDetailsFormView extends Backbone.View template: HandlebarsTemplates['contractors/payment_details_form'] className: 'contractor-payment-details-form-view' events: 'change .payment-method': 'updatePaymentMethod' 'keyup .routing-field': 'updateBankName' initialize: (options) -> @company = options.company ... updatePaymentMethod: -> paymentMethod = @$('.payment-method').val() if paymentMethod is ENVIRONMENT.PAYMENT_METHOD_CHECK @$('#pay-by-check').removeClass('hide') @$('#pay-by-direct-deposit').addClass('hide') else if paymentMethod is ENVIRONMENT.PAYMENT_METHOD_DIRECT_DEPOSIT @$('#pay-by-check').addClass('hide') @$('#pay-by-direct-deposit').removeClass('hide') else @$('#pay-by-direct-deposit, #pay-by-check').addClass('hide') updateBankName: -> routingNumber = @$('.routing-field').val().replace(/_+/, '') if routingNumber.length == 9 $.ajax( type: 'GET' url: API_PREFIX + '/banks/' + routingNumber ).done((response) => if response['name']? @$('#bank-lookup-name p').text(response['name']) @$('#bank-lookup-name p').addClass('name-shown') else @$('#bank-lookup-name p').text('') @$('#bank-lookup-name p').removeClass('name-shown') ) else @$('#bank-lookup-name p').text('') @$('#bank-lookup-name p').removeClass('name-shown')
Although this ultimately accomplished our product goals, the code is hard to make sense of. We've added many extraneous classes and identifiers simply to locate elements on the page. We have several event handlers working simultaneously, and it's difficult to reason about what the UI will look like based on a given state.
We started to question what would happen based on the order of events and how these interactions played together. We started to realize that the DOM was our only real representation of application state, and inherently difficult to use as a source of truth in our code.
Inefficient DOM Manipulation
Another common Backbone-ism is to listen to changes on a model or collection and accordingly re-render the view in response to those changes.
class NavView extends Backbone.View className: 'nav-menu' initialize: (options) -> @user = options.user @company = options.company @listenTo(@company.companyMigration, 'change', @render) ...
While this allows our Backbone views to be a little more declarative and gives us some loose data binding, we are essentially tearing the entire view off the DOM, re-rendering, and putting it back onto the DOM. Not only have we just blown away what is effectively our application state, large scale DOM manipulation is the heaviest thing your client side app can do. Backbone provides few options for optimizing this, aside from handling the manual manipulations ourselves, which we have shown to be difficult to maintain as soon as multiple handlers get involved.
So, what do we do?
We knew we needed a change when we felt our productivity was beginning to suffer. As our views evolved over the years, some of the more complex flows became unmaintainable. Development speeds had slowed down immensely, frustration began to grow knowing that we lacked sufficient tooling for our system, and we knew there were better options out there.
We hope you'll read on!
Comments on Hacker News