Delayed Reaction [My experience converting a jQuery/Knockout.js application to use the React library]
It's important to stay up-to-date with technology trends and popular frameworks. That was part of the reason I wrote this blog using Node.js and it's part of the reason I recently converted a project to use the React library. That project was PassWeb, a simple, secure cloud-based password manager. I wrote PassWeb almost two years ago and use it nearly every day. If you're interested in how it works, please read the introductory blog post about PassWeb. For the purposes of this post, the thing to know is that PassWeb is built on the popular jQuery and Knockout.js frameworks.
To be clear, both frameworks are perfectly good - but switching was a great opportunity to learn about React. :)
Conversion
The original architecture was pretty much what you'd expect: application logic lives in a JavaScript file and the user interface lives in an HTML file. My goal when converting to React was to make as few changes to the logic as possible in order to minimize the risk of introducing behavioral bugs. So I worked in stages:
- Stage one migrated the interface from HTML to a JSX file. This conversion took advantage of the existing observability (via Knockout) of variables to inform the need for UI updates. Observability for application logic seems like a good companion to React's basic design (and efforts like MobX formalize this approach).
- Once the Knockout logic had moved to JSX, stage two removed the Knockout dependency completely by implementing a simple, drop-in replacement for the
observable
API. - Finally, stage three removed jQuery DOM manipulation and network calls via the
ajax
method. Again, I implemented a simple, drop-in replacement forajax
using the XMLHttpRequest API directly. (Other options weren't as small or dependency-free as I wanted.)
Having performed the bulk of the migration, all that remained was to identify and fix the handful of bugs that got introduced along the way.
Details
While JSX isn't required to use React, it's a natural fit and I chose JSX so I could get the full React experience.
Putting JSX in the browser means using a transpiler to convert the embedded HTML to JavaScript.
Babel provides excellent support for this via the React preset and was easy to work with.
Because I was now running code through a transpiler, I also enabled the ES2015 Preset which supports newer features of the JavaScript language like let
, const
, and lambda expressions.
I only scratched the surface of ES2015, but it was nice to be able to do so for "free".
One thing I noticed as I migrated more and more code was that much of what I was writing was boilerplate to deal with the propagation of state to and from (observable) properties. I captured this repetitive code in three helper methods and doing so significantly simplified components. (Projects like ReactLink formalize this pattern within the React ecosystem.)
Findings
Something I was curious about was how performance would differ after converting to React.
For the most part, things were fast enough before that there was no need to optimize.
Except for one scenario: filtering the list of items interactively.
So I'd already tuned the Knockout implementation for better performance by toggling the visibility (CSS display:none
) of unfiltered items instead of removing and re-adding them to/from the DOM.
When I converted to React, I used the simplest implementation and - unsurprisingly - this scenario performed worse.
The first thing I did was implement the shouldComponentUpdate
function on the component corresponding to each list item (as recommended by the Advanced Performance section of the docs).
React's built-in performance tools are very useful and quickly showed the need for this optimization (as well as confirming the benefits).
Two helpful posts that discuss the topic further are Optimizing React Performance using keys, component life cycle, and performance tools and Performance Engineering with React.
Implementing shouldComponentUpdate
was a good start, but I had the same basic problem that adding and removing hundreds of elements just wasn't snappy.
So I made the same visibility optimization, introducing another component to act as a thin wrapper around the existing one and deal exclusively with visibility.
After that, the overall performance of the filter scenario was improved to approximate parity.
(Actually, React was still a little slower for the 10,000 item case, but fared better in other areas, and I'm comfortable declaring performance roughly equivalent between the two implementations.)
Other considerations are complexity and size. Two frameworks have been replaced by one, so that's a pretty clear win on the complexity side. Size is a little murky, though. The minified size of the React framework is a little smaller then the combined sizes of jQuery and Knockout. However, the size of the new JSX file is notably larger than the templated HTML it replaces (recall that the code for logic stayed basically the same). And compiling JSX tends to expand the size of the code. But fortunately, Babel lets you minify scripts and that's enough to offset most of the growth. In the end, the React version of PassWeb is slightly smaller than the jQuery/Knockout version - but not enough to be the sole reason to convert.
Conclusion
Now that the dust has settled, would I do it all over again? Definitely! :)
Although there weren't dramatic victories in performance or size, I like the modular approach React encourages and feel it may lead to simpler code.
I also like that React combines UI logic and presentation better and allowed me to completely gut the HTML file (which now contains only head
and script
tags).
I also see value in unifying an application's state into one place (formalized by libraries like Redux), though I deliberately didn't explore that here.
Most importantly, this was a good learning experience and I really enjoyed getting to know React.
I'll definitely consider React for my next project - maybe even finding an excuse to explore React Native...