July 26, 2017

React without Redux

Shortcomings in Backbone, React without Redux | Mixmax

Background

Mixmax was originally built using MeteorJS, a full-stack JavaScript framework that enabled us to build features extremely quickly. As our product grew in scope, we began breaking our architecture down into microservices to improve performance. Some of the views served by these microservices (such as the compose window email editor we display in your inbox) were fairly static and could be rendered mostly server-side. Since our team was fairly experienced with Backbone and Handlebars and those technologies satisfied our requirements, we used them.

Over time, however, Mixmax's scope has grown significantly. In addition to writing rich emails with our advanced email editor, our users can now see team-wide insights into their communications, create sophisticated email campaigns using Sequences, and share availability using their Mixmax calendar.

As we built these features, we noticed some shortcomings in Backbone:

  • Rerendering Backbone views could cause cursor focus and scroll position to be lost because our Backbone render functions fully replaced the innerHTML of the views' root elements.
  • Rerendering Backbone views would often result in unnecessary work being done (relative to something like the virtual DOM), which meant that users saw more spinners and glitchineess than was necessary when using Mixmax.
  • Configuring Backbone views to rerender when data changed involved writing code that was relatively verbose and complex.

These issues' significance swelled over time. We recently reached a tipping point when implementing double-click-to-rename became a multi-day project and thus decided set out to solve these issues.

Potential solutions

We initially identified three classes of solutions.

Use Backbone differently

Some of our issues were resolvable without changing frameworks. For example, in some cases, to avoid losing scroll position, we could update only a specific part of the UI using view functions rather than rerendering entire, top-level views using Backbone Views' render functions. Unfortunately, this effectively meant writing a custom virtual DOM implementation using jQuery in every Backbone view that needed this sort of updating, which seemed extremely costly and complex. We trialed this approach in one view and proved this costliness to ourselves, then ruled it out.

// ...

// An example of one of our "custom virtual DOM" functions
/** * Updates a template folder's name among the user's list of template folders. * * @param {TemplateFolderModel} templateFolderModel */
_updateTemplateFolderName(templateFolderModel) {
  const id = templateFolderModel.get('_id');
  this.$(`.js-tag-item[data-id=${id}]`).find('.js-snippet-tag-name').text(templateFolderModel.get('name'));
}

// ...

Keep Backbone's data management and replace its rendering

We envisioned an intermediate solution where we would continue using Backbone and our Meteor-like publications libraries (1, 2) that we had integrated with Backbone to manage our client-side data, but replace Backbone's UI rendering. This solution would allow us to continue using all of the Backbone models and collections we had already written, therefore lowering the cost of adopting a new framework.

Replace Backbone entirely

Last, we considered replacing Backbone altogether. The only advantage of this solution we imagined was that we might somehow benefit from being all-in on a new framework. The downside of this approach was that replacing our usage of Backbone entirely would be very expensive in terms of engineering time.

We didn't discover any reasons for this cost being worthwhile in our investigation of frameworks, so we decided on the intermediate solution: replacing Backbone's rendering layer.

Selecting a framework

Considerations

We based our investigation into view framworks on a number of requirements and considerations. Given our choice to replace only Backbone's view layer, we required that our new framework interoperate with Backbone models and collections, not require us to rewrite any existing views to minimize adoption cost, and be mature/well-backed for reliability. Additionally we hoped that the new framework would increase our speed of development, be easy-to-use by designers (ie markup-like), enable us to easily write modular code, and be compatible with our existing client-side routing.

Selecting React

After eliminating most frameworks we came across based on immaturity (eg Preact and Inferno), we narrowed our candidate frameworks down to Ractive, React, Angular, and Vue.

We eliminated Vue because we felt its directives would be less intuitive to designers than JSX or Handlebars-like syntax and because it lacked any apparent, significant advantages over React while being much less mature than React.

We liked Angular for being being widely used in production, but eliminated it based on there not being a straightforward way to integrate it with Backbone and it having a steep learning curve.

We liked Ractive because it shared many of the benefits React offered while having Handlebars-like templating syntax, which we liked and knew well. Unfortunately, Ractive didn't seem to be widely used in production (1, 2) and seemed to be maintained by a small handful of people. We noticed that the project's GitHub issues were not resolved quickly and that there were TODOs in the project's docs, which concerned us.

React proved to be the best option, satisfying all of our requirements and nearly all of our bonus desires. Its disadvantages were that its syntax would be a bit confusing for designers who didn't know JavaScript well and, since we wouldn't be using a centralized data store like Redux, we'd sometimes be forced to pass event handlers down through many layers of components when lifting state.

Mixmax + React

We've developed a number of React patterns since beginning to use the framework.

To integrate React components into our existing Backbone view hierarchies, we've been using ReactDOM.render within Backbone views' render functions.

// ...

const MeetingTypesDashboardView = Backbone.View.extend({
  render() {
    ReactDOM.render(
      <MeetingTypesDashboard />,
      this.el
    );

    return this;
  },

  remove() {
    ReactDOM.unmountComponentAtNode(this.el);
    MeetingTypesDashboardView.__super__.remove.apply(this);
  }
});

// ...

We've been using MongoDB's open-source connect-backbone-to-react library to create components (using a higher-order component from that library) that update when our Backbone models and collections change. Doing so has made integrating Backbone with React straightforward and fun!

We've also been creating "container" and "presenter" components as explained by Dan Abramov. Dividing our React components like this has allowed us to achieve the sort of "view" and "controller" separation we enjoyed with Handlebars templates and Backbone Views.

We've published some gists documenting our React patterns in an example container component and presenter component if you'd like to check out all of them!

In our few weeks with React, we've written two Mixmax Dashboard sections in it: Meeting Types and Rules. We're already loving how easy it is to build reusable UI components and how our struggles with Backbone, like maintaining scroll position across renders, are long-gone.

Shoot us a message if you're interested in writing React with us :)

You deserve a spike in replies, meetings booked, and deals won.

Try Mixmax free