Summary: In this article I will present a new version of the BirdWatch application that uses ReactJS on the client side instead of AngularJS. Don’t worry if you liked the previous AngularJS version - I do not intend to replace it. Rather, I want to create another version of the client side web application in order to get a better feeling for the pros and cons of different frameworks and libraries. So think of it as something like the TodoMVC of reactive web applications. Well, not quite yet, but feel free to write another client version for comparison. EmberJS anyone? For this new version I have also rewritten the barchart as a ReactJS component with integrated trend analysis and no dependency on D3.js. Again, there is nothing wrong with D3; I just like to try different approaches to the same problem.
Here’s an animated architectural overview, mostly meant as a teaser for the previous article which describes the server side of the application in detail. You can click it to get to that article:
This has all worked really nicely with AngularJS for a couple of months. Now let’s see if we can build the same thing with ReactJS on the client side.
Why would one choose ReactJS over AngularJS?
In the current version of BirdWatch, AngularJS decides when to figure out if the data model changes so that it can determine when to re-render the UI. These calls can happen at any time, so they need to be idempotent 2. That requirement has been met, any call to the crossfilter service for data is indeed itempotent, but there’s a catch: every call to get data is potentially expensive, and I’d rather avoid unnecessary calls to the crossfilter service. Instead I want to decide when the client UI is rendered by actively triggering the render process. That way I have full control when and how often the UI renderer is fed with new data.
As discussed in my recently published article, ReactJS may also be a better fit when working with immutable data. That is not a concern in the current version of BirdWatch, but it may well be an issue in the future.
Implementing the existing functionality with ReactJS
There are four main areas of functionality in the application:
Search: The user can start a search by entering the terms into the search bar, which will refresh the data and establish a Server Sent Events (SSE) connection to the server that will deliver search matches in real time. At the same time previous matches are retrieved and merged with the real time results.
Rendering of tweets: Different sort orders of tweets are displayed in a list of what I call tweet cards. In AngularJS, directives handle the abstraction of one such tweet nicely.
Pagination: The application loads many more tweets than can be displayed on one page (with 5000 tweets being the default). The AngularJS version implements this with a modified subset of the AngularUI-Bootstrap project.
Charts: Different visualizations are rendered on the page. At the core, D3 does this for us. In the AngularJS version, relatively thin wrappers make directives out of these charts that get wired data and that re-render when the data changes.
Bookmarkability: users can bookmark a search and come back to it later, send it to friends, tweet about it or whatever. AngularJS provides the $locationProvider for this.
Let’s go through these areas one by one.
In this area, AngularJS and its two-way data-binding shine. The content of the search input element is bound to a property on the $scope, just like the button is bound to a function that is also part of the $scope and that triggers a new search. ReactJS, on the other hand, does not offer two-way binding out of the box. There are helpers to achieve this, notably ReeactLink, but I have not tried it. It also seems that it is generally discouraged. In this case it was fairly trivial to achieve the functionality without ReactJS; instead I am assigning the functionality using onclick for triggering the search function, and jQuery to achieve the same when enter is pressed inside the input field. AngularJS offers more of a full framework solution for such problems, but I am okay with this solution here.
The button is plain HTML with an onclick handler. I have assigned the search function to serve as the handler function, which lives in a property of the global BirdWatch object. In addition to the click handler for the button, I also wanted to be able to trigger a search when pressing ENTER inside the search field. jQuery is perfect for that:
1 2 3 4 5
Finally here is the function that triggers the search:
1 2 3 4 5 6 7 8
Rendering of tweets
This is where it gets much more interesting. AngularJS renders the list of tweets from the data model using ng-repeat like this:
1 2 3 4 5
where cf.tweetPage is a function that delivers the data from the crossfilter object. The application code has little control over when this happens. It will certainly happen when explicitly calling $scope.$apply and also when anything else happens that has any effect on the data model, anywhere. This is what I meant when I said earlier that this may not be the most desirable thing when this function call is potentially expensive.
ReactJS works the other way round. The application instantiates a component for the list of tweets that knows how to render itself, and it will only subsequently do that when the application actively feeds it new data. Let’s look at that in more detail. In the HTML, there is only a single div without any special notation:
Then in the component declaration, it looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
The Tweet component then includes a RetweetCount component, to which it passes the RT count as props. This component has conditional logic in which it decides itself if it wants to return an empty div or actual content. The same goes for the FollowersCount component, which I have omitted here as it follows the same principle.
Unlike in the AngularJS version, where I relied on additional projects, I have implemented this from scratch with ReactJS. Here’s the entire component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
Once again, we have two components, one for each item and one that combines the individual items. In the Pagination component, we first determine the minimum of either the number of pages (passed in as props) or 25 in order to render a maximum of 25 pages. Then we do a map on this the resulting range (with the range being created by an underscore function), rendering one PaginationItem component for each of these pages. So far this is comparable to the components we have already seen above. What is new here is that the handler functions are also passed as props and assigned by the component. The nice thing about this is that this way we can also dynamically assign handler functions. We could just as well call functions on the global application object inside the handlers, but conceptually I find it cleaner to think about the component only ever receiving props, without needing to know anything about the application it is embedded in.
At first I did not really know how to achieve this feature using ReactJS. I have seen examples using Backbone and its router, which would make sense for more complex applications. One such example is this article and another one is this article. For this application, introducing Backbone seemed like overkill though, so I was looking for a simpler approach. Turns out achieving this is super simple using jQuery and the plain old DOM API. For the search function I had already created a jQuery object:
Then inside the search function, I simply set the window.location.hash with a URI encoded version of the search term:
Then when loading the page, I read the location hash into the search field and call search(), which reads the content of the search field and triggers the search with whatever is in there:
Building an SVG Bar Chart with ReactJS (without D3.js)
D3.js is an amazing technology and really great visualizations have been built with it. However it also has a considerably steep learning curve. I personally find ReactJS easier to reason about because unlike D3.js it does not have the notion of update. Instead, we always pass it the entire data and it will put the changes into effect itself through an intelligent diffing mechanism where it compares current and previous versions of a (fast) virtual DOM and only puts the detected changes into effect in the (slow) actual DOM. Now I thought it would be nice if this concept could aso be applied to SVG (scalable vector graphics) in addition to HTML. Turns out the same principles apply, and accordingly I found it fairly simple to re-build the bar chart and have ReactJS instead of D3 create the SVG inside the DOM. The resulting code is much shorter than the previous D3 version despite a lot of added functionality. The previous version was a simplistic bar chart, whereas the new version has a built-in trend analysis using regression-js, a neat little regression analysis library. In this new chart each bar is aware of its history and determines its trends using linear regression. Here’s how that looks like:
Each bar has two associated trend indicators, one to show recent movements in the ranking and the other to show an overall trend of the word occurrence. The trends are determined using a simple linear regression, where the slope of the resulting function directly translates into an upward or downward trend. I don’t have the time to go into detail about the implementation of this chart today, but this topic should make for an interesting article in the future.
ReactJS nicely complements the rendering of the UI of the BirdWatch application. From a bird’s-eye view, it is really not more than a function that accepts data and that, as a side effect, effects a DOM representation in line with the data provided. It does the rendering in a very efficient way and it is low-maintenance; it does not require any more attention than the call necessary to inform it about data changes. I find its data flow model very easy to reason about, simpler in fact than the multitude of concepts one needs to think about when building an application with AngularJS. So far AngularJS has also worked really well for this application, so I’d say both are suitable approaches to single page web applications. For now I’m curious to know your opinion. You can find the source code on GitHub. A live version is available in two versions: using ReactJS and using AngularJS.
Until next time, Matthias
The list of technical terms I use for the live demo under birdwatch.matthiasnehlsen.com easily fits into this cap, in which case the application will receive all these tweets. The term Obama also usually fits into this limit. The term love on the other hand doesn’t. If you were to download BirdWatch from GitHub, create a Twitter API key and replace the list of software terms with only the word love, I bet you will reach the 1% limit any second of the day. However not to worry, Twitter will still deliver at the rate limit. When I last tried it, it was about 4 million tweets per day. Sure, you might lose tweets doing this, but there’s not need to worry when you are looking for popular tweets as they will appear time and time again as a retweet, making it highly unlikely to miss them over time. Only the current retweet count may lag behind when the last update as a retweet was dropped.↩
Idempotent: This basically means that it must be possible to call something multiple times without additional side-effects, if any at all. Idempotency, for example, is also essential in scenarios where some service guarantees an at-least-once delivery. In that case you don’t want to run into trouble (like wrongfully incrementing a counter) when that service delivers more than once.↩