Innovation Series, Tech/Engineering

Key takeaways — Migrating our WebUI from jQuery to React

Until a couple of years ago, the front-end of our web application for Phoenix at Druva was written purely in jQuery. But with the growth of the application and a number of new features being continuously added, we started seeing some challenges in terms of complexity and scaling.

Some of the challenges that we faced with jQuery:

  • Slowness and performance issues when user interaction increased
  • Difficulty in maintaining the growing codebase
  • Code reusability to maintain UX consistency across different products of Druva

So our team started brainstorming new solutions to our front-end architecture. After evaluating a number of different frameworks, we decided to go with React to take advantage of: 

  • Independent and reusable components, which are helpful for maintaining UX consistency by reusing components
  • Virtual DOM (Document Object Model), which updates only the part of the DOM that has changed, not the whole DOM; it overcomes page slowness with increased user interactions 

As Phoenix is continuously changing and already in use by a large number of customers, a complete rewrite all at once was out of the question. We decided to start migrating module by module. New features were added in the new React module as an ongoing part of the migration. For older modules, we kept adding support in jQuery.

Strategy

We brainstormed quite a few ideas about how to plug in React in our existing jQuery code, such as:

  • Keeping two separate apps for jQuery and React.
  • iFrame-based approach — An iFrame (short for inline frame) is an HTML element that allows an external webpage to be embedded in an HTML document. Unlike traditional frames, which were used to create the structure of a webpage, iFrames can be inserted anywhere within a webpage layout. In an iFrame-based approach, the parent page could still be in jQuery, but individual components within the page could be created in the separate react app. For example, an existing user details page that is in jQuery could have child sections like basic user details, charts, etc. These child sections could be built using React and injected as an iFrame in the parent user details page.
  • Keeping a single app for both React and jQuery and rendering corresponding pages based on routes. Routes are the paths to navigate to a page in an application. For example — if you are in /home currently, which can be an existing jQuery page or route, and if you click on a link to navigate to /home/details, it can be a new React page or route.

Of these approaches, the first approach had some drawbacks, like loading the whole app (resources, etc.) on change of routes. In the second approach (iFrame-based approach), we needed to keep two different apps (one for jQuery and one for React). Apart from general limitations of using an iFrame, such as communication between cross-domain iFrames, handling extra scroll bars, etc., communication with an iFramed application becomes cumbersome. Also, there are some challenges like security risks, usability issues, and SEO problems with iFrames. 

So we decided to go with the third approach as our application was already using hash-based routing for rendering jQuery code. Also, we wanted to listen to the same hash change event in jQuery as well as in React app. This helped us keep both the React and jQuery pages separate. 

In addition, we had to decide on how we would incorporate the new requirements along with existing code to be rewritten. With the idea of keeping jQuery and React separate, whenever there was a requirement of a new page/route, we preferred writing it in React, but if it was a change in an existing page, we made changes in jQuery code only.

After deciding upon the strategy, we started with keeping the jQuery application as the entry point and inside that we created a mount point for React.

A snippet containing the starting point of the application (index.html) is shown below:

Routing

As we adopted a hash-based routing, we rendered React and jQuery nodes conditionally based on the current hash. In order to achieve this, we maintained two separate lists of routes, one for jQuery and another for React.

Bundling

For bundling, we explored Grunt, Gulp, and Webpack. Bundling is an optimization technique that can be used to reduce the number of server requests. A bundle is a file format encapsulating one or more HTTP resources in a single file. It can include one or more HTML files, JavaScript files, images, or stylesheets. In our case, currently, we are bundling only Javascript and CSS files to reduce the number of HTTP requests.

Since Webpack can alone do what Gulp and Grunt can combined, and also supports bundling of CSS and images, we decided to go with Webpack. As we had not used Create React App for building our application, we created a custom configuration for Webpack. 

We had to bundle both the jQuery and React code. So, we created separate bundles for React, jQuery, and Vendor files. Our application had approximately 50 routes just for the React code, which resulted in the single React bundle taking too long to load. We decided to group related React routes and created separate bundles for each group.

We used React.Suspense, React.Lazy, and magic comments to Lazy load the bundles based on the routes. React.Suspense allows us to show fallback content (such as loader) while waiting for the lazy component to load. It resulted in multiple smaller bundles per routes, loading the page 2x faster.

We also used Webpack plugins to extract styles into cacheable minified CSS files and Webpack’s SubResourceIntegrityPlugin for resource (JS and CSS) integrity hash.

State Management

After setting up the React code base, we needed something for global state management. We decided to go with Redux for managing the state of our application. Along with Redux, we used Redux Thunk as a middleware for async Ajax calls. 

We kept the Action creators and API layers simple with all the logic being in Reducers, like manipulating API responses to formats required by the UI. 

We also cache data in the Redux layer (wherever data is not expected to change frequently) in order to avoid making heavy API calls repeatedly. 

Error Handling

In order to avoid breaking the application, we used error boundaries. Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.

These are used at two levels. The first at the top level of the application, which is helpful to show users the fallback UI when the application is down. The second is used at the route level to show fallback UI for the route which is down. This ensures none of the other routes are impacted.

Testing

For testing React code, we are using Jest and Enzyme. Jest is the test runner, assertion library, and mocking library. Enzyme provides additional testing utilities to interact with elements. We unit test our React components in order to ensure code quality. 

Conclusion

Here are the takeaways from our tech stack migration:

  • With a decrease in bundle sizes and with bundle splitting, the load time of the application reduced drastically, improving the overall application load time by almost 50 percent.
  • The codebase is now more readable than ever as the role of legacy code has been minimized. ReactJS runs the major part of the application.
  • Even though React is a library, there are some well established patterns, such as common component libraries, stateless functions, render props, HOC, controlled components, and react hooks, etc., which we follow in our codebase. This gives us a better understanding of the codebase, which was difficult to achieve with jQuery. 
  • With the configurations in Webpack, it has become easy to debug in development as well as production environments.
  • Application resources are now more secure with an integrity hash in place.

Since the migration, our team has been continuously working on adapting newer React features as well as making our code cleaner and more performant. 

Next Steps

Looking to learn more about the technical innovations and best practices powering cloud backup and data management? Visit the Innovation Series section of Druva’s blog archive.