Why We Chose React to Help Serve Millions of Educators

We recently decided at TpT that we needed to make some changes in our front-end stack so we can develop faster and achieve cohesion across our UI.

The Problem We Faced #

Like many companies that have been around for a few years, we have a very mature code base. Our site was built primarily with CakePHP templates with some jQuery code sprinkled around to complement the UI with user enhancements and interactivity. This worked perfectly fine when our engineering team was relatively small. In fact, it helped us grow into one of the leading EdTech ventures in the US.

But, if you’re familiar with the ever-changing world of front-end development, you must also be aware of the problems that normally arise when developing large applications with ad-hoc JS handlers or any non-opinionated solution — you quickly end up with tightly coupled JS code to your views which is:

  • untestable,
  • unmaintainable, and worst of all,
  • is repetitive.

In front-end-land, we pay double the price for code which repeats itself:

  1. when we make it difficult to maintain changes as the same logic exists in multiple places. Not only it makes it harder to onboard new developers, but it clearly introduces room for bugs and inconsistencies, and
  2. when we deliver the same functionality to the client twice. This increases our JS payload and is therefore detrimental to our overall site performance.

This is in no way JS’s or even jQuery's fault, but an inevitable human flaw — the bigger our organization became and the bigger our application grew, it became harder to keep track of all the jQuery event handlers, validations, and third-party libraries we've accumulated over the years. So, our Web Platform team was put to the task of finding a solution to our problem which would allow us to decouple view logic from any business logic. We experimented with a few frameworks and eventually decided to go with React as our tool.

Why React? #

While React has been under fire for being too bloated — at least when combined with all of its typical dependencies (i.e. React-Router, Redux, etc.) — and therefore risks slower delivery of the UI to the client, we saw a great potential for React to be helpful when it came to developer friendliness.

For us, it came down to a few points that won us over:

  • React's API is small and simple, which made it easier to on-board developers and get them up to speed with the conventions of the framework.

  • The React community is huge. We rarely need to reinvent the wheel when we’re looking for new functionality. The community is also pretty set on standards and conventions. This means we can spend less time choosing tools, making our own conventions, and debating on tabs-vs-spaces.

  • React is battle-tested. At least from a business perspective. Large companies use it in production which mostly means to us that there is going to be active development and investment in it for a good chunk of the foreseeable future.

  • React offers (kind of) an out-of-the-box solution to server-side rendering (SSR). SEO is a major concern for our online presence, therefore a solution which allows us to deliver fully-rendered HTML was important. Some of our requirements were a bit more involved and required some creativity (half server-rendered React, half legacy PHP templates 🙊), but for full React pages, this was easy to set up.

  • But most importantly, React helps with separation of concerns, state flow among UI elements and the resulting transitions. Its unidirectional data-flow and reactive nature allow for a declarative organization of components in a way that’s natural to anyone who’s worked with HTML. Combined with the typical Flux architecture, it allows for complete separation between the application’s UI and its data.

Consider the following example:

When a user adds a product to their shopping cart, how would we go about updating the cart-item count in our header? In our legacy codebase, this used to be tricky to solve; the physical distance of the item from the item-count in the header also meant there was a good chance they were in completely different files, developed by different people, and followed different conventions. We would end up with something like this:

// deeply nested somewhere in our items page (for illustration purposes only):
$(".item .add-to-cart-btn").on("click", function (e) {
  var $item = $(e.currentTarget);
  $.post("/cart/items", { itemId: $item.attr("data-id") }, function () {
    var $headerCount = $("#cartHeaderCount");
    var previousCount = parseInt($headerCount.text(), 10);
    var newCount = previousCount + 1;
    $headerCount.text(newCount);
  });
});

While the code above probably works, it is suboptimal and is not performant. Each time a user adds an item to their cart we fire a request. We then query the DOM for the header counter element (👎), extract the current count, convert it to a number (👎), increment it by one, and finally modify the counter element directly in the DOM which causes the browser to recalculate layout and repaint the element and all subsequent elements that are affected by its new value and position (👎👎👎🐼).

This code is also hard to maintain. What if the CSS selector for any of the elements changes? What if we need to apply it to another part of the app? Our callbacks aren’t bound to any particular element and so we have to explicitly specify what element they should operate on. And I won’t even get into the difficulties involved with testing it 😫.

React completely abstracts this. When our state changes, then and only then our count component can decide for itself whether it needs to update.

Here is a naïve implementation:

class App extends Component {
  state = { cartCount: 0 };

  handleAddedToCart = () => {
    this.setState({
      cartCount: this.state.cartCount + 1,
    });
  };

  render() {
    return (
      <div id="App">
        <HeaderCount count={this.state.cartCount} />
        <Item id={3} onAddedToCart={this.handleAddedToCart} />
      </div>
    );
  }
}

And comparing React with the jQuery example as illustrated above:

  1. Query the DOM - React does this once for us and then keeps a reference to the DOM elements in memory. We could have implemented a similar “cache” in our jQuery example, but then we would also have to maintain it ourselves. With React, it’s included automatically.
  2. Extract the current count and convert it to a number - We keep our count in a state, a plain-’ol-JS-object, meaning we don’t need to manually do conversion, etc.
  3. Modify the counter element and cause a reflow- Well, there isn’t much React can do about this one. If the count changed, the browser will need to repaint. But, there are some benefits to React here too: if the count didn’t change we can utilize shouldComponentUpdate to let React know we don’t need any DOM updates (more perf optimization here). We also don’t need to query for the header-counter element anymore, and lastly, if we were using Redux here, we could easily batch updates so all DOM manipulations would happen at once.

But performance aside, we’re in it for maintainability, consistency, and reusability. The React code above does not rely on any element selectors, and is completely self-contained. CSS selector changed? No problem 🎉.

Moreover, our newly created <``Item /> component can now be used across the entire application. In our search page, our store page, and our homepage. In all of its instances it will default to the same styling, same functionality, and guess what, it’s already tested!

The React code is also superior to its jQuery counterpart in terms of readability and testability. With libraries like Enzyme (our favorite) or Jest, we can easily simulate user interactions, call component methods directly, and assert against the render method’s output 💯.

What We Still Missed With React #

  • Possibly too much freedom and flexibility. React gave us good conventions and practices, but building a complex application quickly surfaced how its flexibility of components can be a double-edged sword; they’re powerful hybrids of imperative and declarative logic, but this can lead to blurred responsibilities. Whereas traditional MVC frameworks defer orchestration to controllers, React components themselves own this logic. Dan Abramov separates their concerns into “smart” and “dumb” components, but there’s still an incredible amount of freedom in each type of component. Consequently, building out a new component system can quickly devolve into a complex series of debates and proposals, which is why there are so many component libraries — each with their own paradigms and best practices. (Think Grommet, awesome-react-components, et cetera.)

  • Slow server-side rendering. A caveat to React’s wonderful ability to render seamlessly on the server was the somewhat slow, computationally heavy and blocking renderToString function. For pages with complex DOM hierarchy, this was a noticeable issue. There are a few community-built solutions such as Rapscallion, Electrode, or ReactDOMStream which allow for easy component caching and streaming of the HTML. We’re still looking for a perfect solution while trying to defer certain components’ rendering to the client if they’re not vital to our SEO rankings.

  • Global state management. React is very opinionated on how data should flow between its different components (one-way data binding; props are immutable and can only come from the parent, whereas state is mutable and internal to a component), it does not give the developer a way to manage global state (i.e. where should the logged-in user data live?). That includes fetching data from the server. At what point of the component's life cycle should that be done? And where should the data retrieved from the server be stored? That was unclear to us and we needed to find a solution which could be adopted ubiquitously across the team.

    For state management Redux seemed to be the community's choice, and it worked great for us for most global-state use cases. However, at times it seemed a bit cumbersome to manually define an action, plug in a reducer and do the same dance over and over again for simple RESTful data fetching.

    As a result, in addition to barebones Redux, we wrote our own Redux extension (to be open-sourced soon) which lets us abstract some of the seemingly complex concepts of Redux. Now we can define server data dependencies for our React components in a simple and declarative way. Additionally, some of our backend code has recently undergone a migration to a GraphQL API written in Elixir (read about it here), which allowed us to throw React-Apollo into the mix (find out how we use it here). Given it’s merely another extension of Redux, we could still enjoy all of its glory 💃.

In Conclusion #

While some would argue React or its ecosystem isn’t perfect, it helped us increase developer productivity. Its small API, community support, rapid development and clarity on how (most) things should be done, made complex UIs’ development a lot less painful.

Check out our fully migrated product and search pages which are viewed millions of times per day by teachers from all over the world 🙌. And don’t forget to follow us here and on Twitter for more blogposts and updates on our progress as we continue to migrate our stack.