Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Converting CALC from JQuery to React + Redux

Converting CALC from JQuery to React/Redux

At the end of 2016, the CALC team experimented with converting the single-page app on its front page, referred to as the Data Explorer, from JQuery to React + Redux.

Rationale

We inherited the 1350-line explorer.js from a previous team. This code was written under different project and time constraints, and it functioned well for over a year. However, we were fearful of modifying it due to poor test coverage--there were a few Selenium/WebDriver tests for it, and virtually no unit tests.

Understanding how data flowed through the app was also confusing; state was maintained in lots of different places and there was no "single source of truth". This was evidenced by a number of subtle bugs in the app, such as buggy browser history and erratic form controls.

Furthermore, a lot of rendering code consisted of directly setting a DOM node's inner HTML via jQuery's .html() method, which led to XSS vulnerabilities.

As our project's needs turned towards potentially improving the functionality of the Data Explorer, we thought the code could use either a full rewrite or significant refactoring, but we weren't sure which path to take.

The first failed attempt

As two of our team members were experienced with React, and had read about Redux, we thought these technologies, and the architectural patterns they engendered, might be a better fit for the Data Explorer. We thought that perhaps we could encase the whole app in one giant React component that still used the same JQuery-based code inside, and could then chip away inside it somehow.

This "top-down" approach proved unsuccessful, but we were able to accomplish one very useful thing: we linted explorer.js according to the Airbnb linting rules we were using elsewhere in our project.

Refactoring

One of the most beneficial aspects of the linting was the conversion of var declarations into let and const; this gave us important guidance on how we could refactor the app.

We used this knowledge to modularize explorer.js, factoring out six separate modules from it. The code felt less overwhelming and the modularization made the data flow easier to understand.

We were pretty confident about the linting and modularization we had done, so we merged them into our main branch.

Introducing React

It was at this point that we decided to try introducing React again.

However, instead of the "top-down" approach, we tried a "bottom-up" one: we targeted one tiny part of the user interface and converted it into a React component.

One of the biggest benefits of this refactoring was how much easier reading the code became: rather than having to refer to an HTML file to see the original state of the widget, and then see the jQuery code to infer how that DOM changed over time, we could simply read the component's declarative render() method and instantly understand how the component would look given any state. Furthermore, due to the security-conscious design of JSX, we were confident that our new code would be resilient against XSS attacks.

While we were successful at this one tiny change, we weren't sure how successful following this strategy of "chipping away" at the app would be. It still felt very monolithic and intertwined: would we eventually reach a point where we'd just have to throw up our arms in defeat, and all would be for naught?

Introducing Redux

At this point we tried introducing Redux into the app, using its store to drive props to our one tiny React component.

Though one engineer was familiar with the Elm Architecture that inspired Redux, none of us actually had hands-on experience with Redux itself. Introducing it into the codebase in this piecemeal way made it easy for us to acquaint ourselves with it and understand its philosophy.

Chipping away at it

We continued to chip away at the app in an exploratory way. When we couldn't think of a way to pull out a new React component, we'd just refactor things until we could see a way through.

One of the core principles of this gradual evolution, however, was that the app was always to remain production-ready: we rarely made any commits that broke the build or regressed the app's functionality in any way.

A key part of this strategy was creating temporary mechanisms that allowed the legacy code and the new React + Redux code to co-exist peacefully. For example, the legacy code used a library called formdb to maintain some of its state in the DOM; to ensure that it played nicely with our Redux store, we created a temporary Redux middleware that synchronized the states of these two separate stores, allowing us to gradually "wean" the app off formdb. Once none of the code relied on formdb anymore, we were able to do away with the temporary middleware too.

Introducing Jest and Enzyme

Once we became fairly confident that our chipping away would eventually lead us to a better place rather than a brick wall, we introduced Jest and Enzyme to the project's testing repertoire. Each test was written by someone who didn't write the original code being tested, which provided them with an interactive way to familiarize themselves with it.

The big merge

Despite the fact that the app was (almost) always in a production-ready state, it wasn't until fairly late in the process that we felt confident that the React + Redux approach would be successful.

Combined with the fact that the Data Explorer still wasn't a very high priority for our team, all of this work did actually pile up into one giant pull request. However, it was also being constantly monitored by the team and discussed as a work-in-progress PR, so when the time came to review it for merging, it wasn't too overwhelming.

Lots of legacy

Actually, at the time of this writing, in March 2017, there's still a fair amount of legacy code in the Data Explorer.

Third-party JQuery widgets were used for an autocomplete field and a slider, so these were wrapped into React components that largely delegated to the JQuery code via React's various component lifecycle methods. The autocomplete field is being migrated to React because we'd like to enhance it eventually, but we're not sure about the slider.

Furthermore, a histogram at the heart of the Data Explorer is implemented in D3, and we don't currently have a compelling reason to migrate it over to React.

Only time will tell.

Conclusion

Ultimately we were pleased with the Data Explorer's migration to React + Redux. While the total number of lines of code is likely greater than the previous explorer.js and static HTML, the conversion came with the following benefits:

  • As mentioned earlier, the declarative approach of React made it made it much easier to understand what the DOM was supposed to look like at any given point in the app's lifecycle.

  • The unidirectional data flow encouraged by React and Redux made it much easier for us to reason about how our app's state changed over time. It also made it easier to ensure that all representations of the app's state--including views and the current URL--stayed in-sync, which was a problem with the legacy codebase.

  • While the Data Explorer does still require JavaScript to function, we do have a potential path towards progressive enhancement, as the initial rendering of our React components can potentially be done on the server-side.

  • Test coverage is much better than it was before, though we still have a long way to go. That said, the modular nature of React components and Redux reducers/middleware have made it much more straightforward to improve our test coverage: we have a clear path forward that we didn't have before.

  • The modular approach of React and Redux have also made it much easier to understand the codebase and separate concerns in a logical way.

  • As mentioned earlier, the nature of JSX made it much easier to mitigate XSS attacks while preserving source code readability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment