Seamless migration of a monolithic frontend
Some time ago, we had to modernize the frontend monolith of a large high-loaded system running 24/7 transferring it from the outdated Knockout framework to modern React. We want to share our experience on how to facilitate such a project.
The challenge arose when the used architecture became outdated and inadequate to business requirements. The team developed new features, but it was almost impossible to implement them in the existing architecture.
The main part of our product is a single business scenario, whose individual modules are closely interconnected. It can’t just be rewritten from scratch as it is a separate project with its own funding. And while the development process is underway, we will have to maintain two versions at once, one of which will also be unserviceable, since none of the parts of the application is valuable on its own, without any connection to the other parts. In view of that, we started rewriting it module by module and window by window.
One of the key requirements of the project was that the transition should be seamless for users. That’s why all the changes were hidden inside: our developers had built data exchange between the old and new framework, debugged it upon testing that revealed some faults inherent in such a non-standard architecture.
When we began to select options for upgrading, we settled on React. Now it is one of the most popular frontend development tools in the world. It interacts well with other tools that may be needed in each specific project, allowing to create scalable web apps of any level of complexity.
Next, let’s talk in more detail about the specific challenges we had to face during the migration and how we met them.
How to build a target architecture
You can’t do without temporary solutions in such a project, but they all have to be on the old side (that is, in our case, on the Knockout side). React modules immediately work the way we want them to, and other product components adjust to them.
Hence the rule is as follows. All data comes from React that defines the format and content. The task of integration wrappers is to adapt this data for processing on Knockout.
When the migration process is over, all crutches along with the old modules are removed from the system by switching the feature toggle, and the service immediately works as it should.
How to make two generations of frameworks work together
Knockout and React handle data in fundamentally different ways. The challenge is to make sure that all modules, no matter what they are written on, understand in time what happens to the data and how it changes.
To enable older Knockout modules to work with React components, the team created integration bindings. As a result, data can be changed on both the Knockout side and the React side. The cyclic script tracks changes in both parts of the application to handle such situations properly:
- When you change data on the Knockout side, the changes are immediately pushed to React.
- If the change request comes from the React side, the changes are first applied to Knockout and then routed back to React according to a well-established scheme.
This saves us from the threat of an endless loop, where a change in Knockout data causes a change in React, and the resulting change in React causes it again in Knockout.
How to account for changes in different layout modules
React will “think” that the component is still on the page, will try to do something with it, change data, send requests, etc. As a result, the memory leaks and the interface freezes.
To prevent this, the team has implemented a mechanism to track changes in the layout using the browser-based MutationObserver API. When a container with a React component disappears, it calls the necessary methods to have that component unmounted by React as well.
With these two integration mechanisms, we were able to combine React and Knockout components in a user script. As a result, the user walks sequentially through the script and no data is lost. You can go back from any step and all the information you entered will be on the screen as you expect to see it. And the final window with all the options is displayed correctly. We can rewrite a large and complex application step by step instead of writing code only to set it aside for a year or two.
How to simplify testing and approval of UI elements
It is crucial for the team having the ability to quickly develop and test UI elements. We use the Storybook library for this. It’s a separate web page with its own interface, which is like a kind of store for all the UI components we develop. They exist in Storybook separately from the main appl. This approach allows us to firstly remove the dependency on the backend and the environment, and secondly, it’s easy to emulate any scenario with mock data.
The block on the left collects all the elements that are on the page being developed. The designer can drag and drop them, combine them, and see how different components work together and individually. The sliders at the bottom can adjust the appearance, and the adjacent tab contains all the possible actions that can be done with the layout.
When working in Storybook, our backend module automatically stops sending real requests and returns mock data instead, even though everything looks exactly the same from the component side. We have developed mechanisms for generating test data based on the Faker library. If there is a new component that needs to get user or document data, you don’t need to create new stubs and maintain them in multiple files throughout the project in case of contract modifications. It’s enough to call the appropriate mock generation method and the test data will be ready, and in case of changes you only need to maintain it in one place.
In addition, the use of Storybook allows for intermediate acceptance of the visual components of the system. For this purpose, we deployed it in the UAT environment, where all tasks are accepted by the customer. By looking at the Storybook, our customers can see at any time how components are being developed, what new components have appeared and what has changed in the old ones.
Every alike project is unique, a lot of details depend on which particular framework you’re migrating from. Two top tips from our front-enders:
- Move immediately to the ideal picture of the world and build all the compromises on the side of the framework, which you give up.
- Create a comfortable environment for testing, use Storybook-like tools.