market
Storylark Squawks

From Angular to React

Web development is evolving at a rapid pace. There is a constant churn of technologies (remember Flash?), and it's easy to make a decision which seems good at first, but eventually goes awry. Storylark.ph is no exception to such developments.

The primary idea behind our choice of technologies is to make it as easy as possible for people to access their comics and discover new ones. That means the website has to be fast. In particular, we chose AngularJS for Storylark.ph's front-end, but we've made the decision to port it to React. The rest of the post details background on the current architecture and our reasons for porting, so be prepared for a lot of technical content.

Architecture Overview

The Storylark.ph server is written in Scala and uses the Play web framework. Comic images are stored in a directory, served by nginx, with the rest of the data stored in PostgreSQL and Redis.

As previously mentioned, we want Storylark.ph to be fast. In line with this, we implemented the site's front-end as a single-page application using AngularJS. In single-page applications, the server sends a template page for every page in the site. The script in the page then detects its current path and asynchronously loads the rest of the HTML, JavaScript, images, and CSS. When the user tries to navigate to another section, the page requests the additional resources needed to show the page and not need to reload everything. For example, when navigating between two different author pages, the only new data we request should be other author's details and not the header, footer, etc.

AngularJS

Our particular implementation uses AngularJS to do a lot of the processing client-side. With AngularJS, a lot of the work is done for you under the hood. This allows the developer to almost immediately get started on creating dynamic templates and logic.

<!doctype html>
<html ng-app>
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
  </head>
  <body>
    <div>
      <label>Name:</label>
      <input type="text" ng-model="yourName" placeholder="Enter a name here">
      <hr>
      <h1>Hello {{yourName}}!</h1>
    </div>
  </body>
</html>

(taken from angularjs.org)

Curly braces indicate data binding which syncs HTML elements to application models. ng-bind/ng-model are AngularJS directives, which are extensions to HTML. These, together with other AngularJS components, make it easy to generate dynamic pages without manipulating DOM elements manually.

As mentioned before, AngularJS directives extend HTML. It can add additional HTML elements or extend elements to recognize attributes like "ng-repeat" and "ng-bind", allowing the addition of special logic and behavior to them. They turn the mundane task of DOM manipulation in JavaScript to simple HTML-like templates. For example, ng-repeat

<ul>
  <li ng-repeat={{i in items}}>{{i}}</li>
</ul>

creates one instance of the template it's applied to per item in a collection. Compare this to a similar implementation using jQuery:

parent = $("<ul>"):
for ( var i = 0; i < items.length; ++i ) {
    parent.append($("<li>").text(items[i]));
}

One must note that the two implementations are not identical. The AngularJS implementation handles updates to items after the HTML has been rendered. The jQuery implementation just handles the initial rendering and breaks when items need to be updated.

In addition to the above optimizations, by using HTML+AngularJS templates, we also no longer need to recompile every single time we make a change to a template, which is the case with using Play's .scala.html files, giving development a noticeable speed boost as well.

All of this magic, though, adds a layer of difficulty when trying to do more complex things, like taking into account values received from outside the AngularJS life cycle; we ran into this pretty often, for example, whenever we had timeouts, needing to manually trigger the Angular life cycle's $apply and $digest functions ourselves. Scope management also tends to turn problematic, with some values being implicitly (sometimes unintentionally) passed from the parent scope, or other values going missing due to the accidental creation of a new scope (a key difference to note when deciding whether to use ng-if or ng-show).

The biggest issue for us, though, was getting search engine optimization (SEO) to work properly. Because we rely on asynchronous requests to retrieve much of our data, the lack of JavaScript execution leaves crawlers looking at... not a lot, really.

(we're big fans of Bioware and Dragon Age, but there's gotta be a better way of doing this)

This is especially problematic, because the comics have to be discoverable. This means making sure that, at the very least, the comic's metadata has to properly appear (e.g. when accessed by Google, Facebook, or Twitter). When people hear about a comic, the first thing they do is search for it online. If Storylark.ph can't be indexed by search engines, then it won't even show up at all!

(There was a time when Storylark.ph wasn't even showing up here.)

This issue isn't unique to AngularJS. Fortunately, there are solutions for single-page applications to be indexable by search crawlers. The key to this is processing the JavaScript before serving it to the crawler. When the crawler comes across a page marked as having AJAX content (according to the specifications), it instead accesses a different route, one that instead returns a pre-rendered version of the page, ready to be crawled and indexed. Nota bene: for our solution, the HTML that comes out is purely for the crawler's benefit, and isn't formatted for use by regular humans.

This all sounds good, but it's not without problems. Our implementation uses PhantomJS which functions as a virtual browser to generate the pre-rendered pages as they are requested (i.e. just-in-time). However, it was slow to the point that link previews on both Facebook and Twitter timed out upon initial access. To avoid this problem, we had a background task that pre-renders all the title and issue pages upon deployment and caches the result, so they're all ready to be served the moment an actual crawler arrives. After that, we update it whenever a new publish request is approved, to keep cached data from getting stale.

That fixes it! It's a lot of heavy work, though. It's slow. Most importantly, it's not scalable as the number of titles and issues grows.

It's for that reason we considered using React.

React is a JavaScript library for creating user interfaces, initially made for use by Facebook and Instagram. We're also reorganizing Storylark.ph to use the Flux architecture that complements React — not something we're going to delve into deeply, but simply put, it's the prescribed way to organize, send, and store data for React applications.

A major feature of React is that it doesn't need the entire web browser stack to render pages; a Javascript engine would suffice. So, instead of passing the pages to PhantomJS to render, we utilize Java 8's Nashorn Javascript engine to pre-render the page. Moreover, the pre-rendered pages are valid and can be recognized by React so we can serve them straight to all browser clients.

Storylark on React

So far, porting Storylark to React has been mostly straightforward, usually involving the following steps:

  • shoving a template body into the render function of its corresponding React component
  • translating standard AngularJS control structures (ng-if, ng-repeat) into their respective React counterparts (which is basically native JavaScript embedded in JSX)
  • moving controller logic into various React component lifecycle functions
  • transforming AngularJS services and element directives into a mix of Flux stores, React components, and helper utilities
  • attribute directives commonly translate easily into mixins

Being forced to think in terms of components instead of directives has helped with ensuring proper encapsulation. Minimizing state and being strict with each component's properties (PropTypes) also helps with making sure code is easier to debug.

Fortunately, very little has had to change in the back-end to help with the migration. Most of our improvements to the back-end like switching to webpack would have been made anyway had we decided to stick with AngularJS.

At the moment, we're using the following inside our React components:

  • marty.js as an implementation of Flux stores
  • React Catalyst, primarily for data-binding
  • React Router for (surprise!) routing
  • the standard React addons for various utilities such as classSet (soon to be replaced with classnames)

This list will probably grow as we move forward, but one of the more noticeable things with React is that it integrates more easily with other libraries compared to AngularJS and thus doesn't feel like it's a monolithic framework trying to solve all the problems at once.

This isn't our first experience with React. One of our projects, Sakay.ph, already uses it. Do check it out if you commute around Metro Manila!

Interested in the technologies we're using, or want to hear more from us? Follow us on Facebook or Twitter! Want to work on Storylark, or any of our other projects? Check out our careers page.