React-ifying-BrowserStack-Frontend

One of our oldest and most popular products (among developers) is BrowserStack Live. It’s an interactive cross-browser testing tool, built during a period of rapid development in the earliest phases of our company; a whirlwind of whiteboards and releases that allowed us to keep up with our customers’ expectations and their evolving testing needs.

8 years, thousands of lines of code and innumerable bug fixes later, Live is used by over 2 million users across the world. Inevitably, the codebase grew too large to scale further; frequently delayed releases and the number of hours we spent revisiting old issues were proof of that.

Codebases have life spans, and clearly, it was time for us to take stock of ours.

Live: Behind the looking glass

Our frontend stack had 3 major shortcomings that made it difficult to work with and scale:

  • Tightly coupled UI & business logic: We had far too many bloated global objects, as well as several UI elements that were being controlled by different parts of the code.

  • Legacy code: Over the course of 8 years, the technology landscape had shifted away from the tools we had chosen in 2012 (direct DOM manipulation through jQuery). Now we had unused code and libraries within the app that were beginning to create performance issues.

  • Lack of standardization: In the early days of Live, we overlooked standardization to ship faster. 8 years later, we have 4 different product teams following their own standards. The few reusable methods we had were lost in the chaos and developers ended up writing the duplicates, over and over again.

It was grueling work; messy code isn’t easy to wade through. But we weren’t about to throw away the whole codebase and start from scratch. That’s something you never do.

Besides, we could solve the first two problems using React.

Enter React

React has modular error handling, incredible community support, and great unit and integration testing libraries. It was the perfect framework to use for our frontend revamp—IF we could figure out how to:

  • Introduce React inside a heavy jQuery codebase.
  • Update a particular component without interfering with the jQuery implementation.
  • Store, share and update the states.
  • Do all of the above, without affecting user experience and upcoming releases in any way.

Reactor 1.0

To introduce React in our codebase, we came up with a middleware that would allow it to talk to the jQuery layer. We call it Reactor: it exposes the state and actions which allow us to talk to the React component. Additionally, to use our shared state and keep a single source of truth, we introduced a Redux ecosystem. It provides us with an immutable store driven by pure actions to update the visual and logical aspects of the app.

To see how the ‘reactive' part would fit in with our framework, we decided to test the waters by porting the IP Geolocation component in the Live toolbar. It was independent of other major components, so the risk of breaking the application and UX apart was minimal.

Letting the UI react to state changes looked something like this:

/**
 BEFORE (through jQuery & Global objects)
*/

// A jQuery method in global IpLocation object 
function setCountryAsNone() {
 // 1. Instead of checking the state from global objects
      if (DockLocation.lastConfirmedIP === 'none') {
 // 2. Then changing the DOM to reflect the change
      $('.countries__item #no_ip').prop('checked', true)
  }
}
 // 3. And manually invoking the method everytime where check is required, for example:
    IpLocation.setCountryAsNone() // IpLocation is initialized
    IpLocation.setCountryAsNone() // A new session gets started
    IpLocation.setCountryAsNone() // Local testing gets enabled

/**
 * AFTER (through React & Redux state)
 */

// A functional React component that shows no country is selected
    const CountryAsNone = () => {
 // 1. We get the current state through the redux store
    const isIpNone = useSelector(selectorReturningCurrentIP) === 'none'
 // 2. and then updating the part of the DOM with our change
    return <input checked={isIpNone} id="no_ip" type="checkbox" /> 
    }
 // 3. There's no step 3! :D
// React handles all the UI reactively wrt the state.

It worked like a charm: Once ported to React, the component worked seamlessly with the rest of our system. Time to move on to the real deal.

React-ifying the (entire) BrowserStack front-end: The highs and lows

Because Live is a high-velocity, high-usage product, holding back feature releases or accidentally breaking the app/UX apart while moving it to React wasn’t in the cards. We were going to migrate in phases, all the while maintaining compatibility between jQuery and React.

There were two more constraints:

  • Due to the tightly coupled UI-and-business logic, we needed to scope the features carefully and make sure the older jQuery code doesn’t mutate the current React component states.

  • Our user experience had to remain stable while migrating, which meant keeping legacy CSS code from affecting any of our current styles. (Pro tip: Using BEM to style the components worked well for us.)

Some backend APIs we needed during development weren’t in place when we began (we were moving from MVC to API on the side). So we created a mocker with Node.js, Express.js, and Faker.js. With data mocking in place, we had an easier time running and testing our dashboards during development.

To paint out the independent React components to the DOM, we used portals and maintained the same DOM structure to ensure that our E2E tests don’t fail. We also threw in React Suspense to lazy load the portals on-demand and saw a significant reduction in the initial page load time.

During the migration, we introduced a single global and built Reactor, our magnum opus. We moved to webpack and implemented chunk-based loading to optimize the JavaScript & CSS file size to make the whole thing blazing fast.

Long road ahead

We began modernizing our codebase in the summer of 2019. Almost a year later, we've made significant progress towards completely React-ifying our frontend.

Our long-term vision is to move away from the MVC structure to APIs and implement a single store for all four of our products. This would make our different dashboards more cohesive, allowing end-users to switch between products in no (perceivable) time.