An Unconventional Review of React

I liked it. I didn’t expect to.

For the August and September Let’s Code JavaScript specials, I reviewed React.

React, in case you’re not familiar with it, is a library for front-end web development. You use it to create components: little not-really-HTML tags that you can compose together to create your UI.

React is best known for its controversial decisions: implementing a virtual DOM, defining UI in JavaScript rather than templates, and inventing a little superset of JavaScript called JSX that allows you to embed not-really-HTML directly into your JS code.

These decisions combine together to create a world in which, rather than writing code that manipulates the DOM—add this element, fade this one out, update that text field—you instead write code that describes how the DOM is supposed to look right now. React does the hard work of figuring out what manipulations are needed to make the DOM actually look that way.

For example, in the source code I produced for the review, there’s a spreadsheet-like table that has to change whenever several configuration fields change. You might expect this code to be a mess of DOM manipulation logic, but actually, there’s no code to manipulate the table at all. Here’s all the code that’s responsible for rendering a row of that table:

/** @jsx React.DOM */
// Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
"use strict";

var StockMarketTableCell = require("./stock_market_table_cell.js");

var StockMarketTableRow = module.exports = React.createClass({
  render: function render() {
    var year = this.props.stockMarketYear;

    return <tr>
      <StockMarketTableCell value={year.year()} />
      <StockMarketTableCell value={year.startingBalance()} />
      <StockMarketTableCell value={year.startingCostBasis()} />
      <StockMarketTableCell value={year.totalSellOrders().flipSign()} />
      <StockMarketTableCell value={year.capitalGainsTaxIncurred().flipSign()} />
      <StockMarketTableCell value={year.growth()} />
      <StockMarketTableCell value={year.endingBalance()} />
    </tr>;
  }
});

That’s what React’s about, and what makes it stand out. It works. The real question: is it good?

Some Unconventional Review Criteria

Typically, when you read a review of a front-end framework or library, you’ll read all about its size, or which famous names use it, or its performance. And, sure, those things matter. But the most important question for me is simpler and more vital:

Over the 5-10+ years I’ll be supporting my product, will this code cause me more trouble than it’s worth?

It’s not about how much work the library saves me in initial implementation. Oh, no. That’s trivial in comparison. No, it’s about much maintenance it costs me over the life of my app.

I have five criteria I consider.

  1. Lock-In. When I decide to move on to a newer or better framework (or library), how hard will it be to switch?

  2. Opinionated Architecture. Can I do things in a way that best fits the needs of my app, or do I have to conform to the framework’s pre-canned approach?

  3. Accidental Complexity. Do I spend my time thinking about my problem, or wrestling with the framework?

  4. Testability. Can I test my code easily, without excessive mocking?

  5. Search Engine Compatibility. Am I going to have jump through insane hoops to get search engines to index my site?

I rated React’s performance in each category with a ☺ (yay!), ☹ (boo!), or ⚇ (it’s a toss-up).

1. Lock-In: ⚇ (It’s a toss-up.)

Let me be clear: when you decide to ditch React, you’ll be rewriting your UI from scratch. There’s no practical way to hide React behind an abstraction layer, and React UI code doesn’t look like anything else. That’s some pretty hefty lock-in.

So why doesn’t React get a frowny face?

React has two saving graces. First, React’s design encourages putting application logic outside the UI, so at least you won’t be rewriting your whole app.

Second, the React API is relatively small. There’s just not a lot that you need to interact with (see point #3), which means there’s fewer things to break when React upgrades. Also, React’s easy to target at a subset of your page, so you can incrementally migrate to or from React when you need to.

Among UI frameworks, this dismal showing puts React ahead of the pack. But to reiterate: when you ditch React, your investment in UI code goes up in smoke. You’re locked in.

2. Opinionated Architecture: ☺ (Yay!)

React thinks of itself as a library, not a framework, and it shows. React doesn’t dictate your application architecture at all. I had no trouble interfacing my pre-existing and intentionally weird application code to React.

Some people think of opinionated architecture as a good thing. “An opinionated framework helps me understand how to structure my code,” they say. I prefer the opposite. Application structure should be dictated by the needs of the application. There’s no way for a framework to predict what those needs will be, and they’ll change as your application grows.

React does have an associated architectural pattern called Flux, but it’s completely optional. It’s a way of thinking about how to structure your code, not something that’s built into React itself. That’s as it should be.

3. Accidental Complexity: ⚇ (It’s a toss-up.)

React has a delightfully small surface area, and I found it pleasantly free of gotchas. There are only a few key terms to learn (the difference between “props” and “state”), just a few key concepts (how to manage state; treating your render method as immutable), and a bare handful of methods to implement on a typical component. My most complex component implemented a whopping three React methods; most just implemented render().

The fly in the ointment is React’s virtual DOM. It’s an impressive accomplishment, and key to making React work… and it’s all but guaranteed to be a leaky abstraction. Any time you use a new or advanced feature of the DOM, you’ll risk fighting against React. For example, CSS animations seemed to cause problems for a while, and focus management looks a bit dodgy as well.

At the moment, the React team seems like they’re staying on top of things. But what about five years from now, or ten, when React is no longer the new hotness? Browsers keep evolving, and before choosing React for your project, ask yourself if you’re confident React will keep up throughout your application’s entire life.

4. Testability: ☺ (Yay!)

React’s testing story feels a bit immature yet. It just barely earned its smiley face.

At first glance, React provides a nice, simple testing API that has everything you need. You can render a component and search through its DOM tree using a nice, if verbose, set of utility functions that fail fast. You can simulate events. You can mock components, and I was particularly pleased that I never needed to.

The documentation’s a bit thin, missing the examples and design recommendations found in the rest of the React documentation, which contributes to its feeling of immaturity. The biggest problem, though, was that I couldn’t find any way to compare components. Even in a relatively simple application, your components will contain components, and you don’t want your tests to have to know about the implementation details of any component but the one under test.

For example, my ApplicationUi component contains a StockMarketTable component. I wanted my tests to check that the table updated when the application configuration changed. To do that, I wanted to compare the actual table with my hard-coded expectation:

it("updates stock market table when user configuration changes", function() {
  config.setStartingBalance(...);

  var expectedTable = <StockMarketTable stockMarketProjection={projectionFor(config)} />;
  var actualTable = TestUtils.findRenderedComponentWithType(app, StockMarketTable);

  // But how do I compare them?
});

I ended up working around the problem by digging into React’s private implementation details, rendering the components out to static HTML, and comparing the result. Here’s the code.

function checkComponent(actual, expected) {
  var actualRendering = React.renderComponentToStaticMarkup(actual._descriptor);
  var expectedRendering = React.renderComponentToStaticMarkup(expected);

  expect(actualRendering).to.equal(expectedRendering);
}

It works, but it results in terrible error messages when it fails, and relies on internal React implementation details. (I had to visualize React’s run-time object graph in Object Playground to figure it out!) There may be a better way—there should be a better way—but I couldn’t find it.

Other than this not-so-little issue, React was a joy to test. Check out the ApplicationUi tests. They’re marvelously simple and straight-forward, and not a mock in sight. No test is more than two lines long, and this is the most complex component in the entire application.

5. Search Engine Compatibility: ☺ (Yay!)

React’s accomplished a minor miracle: they’ve created a front-end framework that has no search engine issues.

Because of the virtual DOM, React apps can be rendered server-side using Node.js. Not only does this mean that you only need one rendering pipeline for both search engines and real clients, it also means that your page can appear as soon as it’s served. No waiting for the document ready event or for all your JavaScript to load. React can even serve static markup and run without any client-side code at all.

Kudos.

Summary: Recommended.

React is a surprisingly well put-together library. I found it to be simple, intuitive, and usable. If you’re looking for a front-end library for rich applications, or even rich widgets on an existing page, React is well worth considering. It’s not perfect, and the problems that could arise from a potentially leaky DOM abstraction deserve careful thought, but it’s very good. Recommended.

If you liked this essay, you’ll probably like:

comments powered by Disqus