Evolving JavaScript Part 1: Goodbye Backbone

Note: This is the first post in a multi-part series about the evolution of our Javascript application over time. Here are parts 2 and parts 3.

The advancements in the JavaScript community over the last few years have been staggering. With the release of powerful new tools, languages, and frameworks, many frontend development teams have suddenly found that much better technologies are available to them. Here at Gusto, we took advantage of this and made a switch from Backbone to React.

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.

CoffeeScript was an obvious choice coming from a Ruby background - not only did it abstract away many of the overly verbose expressions in JavaScript (and provided class support!), it was shipped by default with Rails.

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:

Non-Trivial Composability

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.

The next post in this series, titled Evolving JavaScript Part 2: Hello React, outlines our decision to move to React and the challenges we faced during the migration.

We hope you'll read on!

Comments on Hacker News