Frankenstein Migration: Framework-Agnostic Approach (Part 2)
In this article, we’ll be putting all the theory to the test by performing step-by-step migration of an application, following the recommendations from the previous part[3]. To make things straightforward, reduce uncertainties, unknowns, and unnecessary guessing, for the practical example of migration, I decided to demonstrate the practice on a simple to-do application.

In general, I assume that you have a good understanding of how a generic to-do application works. This type of application suits our needs very well: it’s predictable, yet has a minimum viable number of required components to demonstrate different aspects of Frankenstein Migration. However, no matter the size and complexity of your real application, the approach is well-scalable and is supposed to be suitable for projects of any size.

For this article, as a starting point, I picked a jQuery application[6] from the TodoMVC project[7] — an example that may already be familiar to a lot of you. jQuery is legacy enough, might reflect a real situation with your projects, and most importantly, requires significant maintenance and hacks for powering a modern dynamic application. (This should be enough to consider migration to something more flexible.)
What is this “more flexible” that we are going to migrate to then? To show a highly-practical case useful in real life, I had to choose among the two most popular frameworks[8] these days: React and Vue. However, whichever I would pick, we would miss some aspects of the other direction.
So in this part, we’ll be running through both of the following:
- A migration of a jQuery application to React, and
- A migration of a jQuery application to Vue.

Code Repositories
All the code mentioned here is publicly available, and you can get to it whenever you want. There are two repositories available for you to play with:
- Frankenstein TodoMVC[10]
This repository contains TodoMVC applications in different frameworks/libraries. For example, you can find branches likevue,angularjs,reactandjqueryin this repository. - Frankenstein Demo[11]
It contains several branches, each of which represents a particular migration direction between applications, available in the first repository. There are branches likemigration/jquery-to-reactandmigration/jquery-to-vue, in particular, that we’ll be covering later on.
Both repositories are work-in-progress and new branches with new applications and migration directions should be added to them regularly. (You’re free to contribute as well!) Commits history in migration branches is well structured and might serve as additional documentation with even more details than I could cover in this article.
Now, let’s get our hands dirty! We have a long way ahead, so don’t expect it to be a smooth ride. It’s up to you to decide how you want to follow along with this article, but you could do the following:
- Clone the
jquerybranch from the Frankenstein TodoMVC[12] repository and strictly follow all of the instructions below. - Alternatively, you can open a branch dedicated to either migration to React[13] or migration to Vue[14] from the Frankenstein Demo[15] repository and follow along with commits history.
- Alternatively, you can relax and keep reading because I am going to highlight the most critical code right here, and it’s much more important to understand the mechanics of the process rather than the actual code.
I’d like to mention one more time that we’ll strictly be following the steps presented in the theoretical first part[16] of the article.
Let’s dive right in!
- Identify Microservices[17]
- Allow Host-to-Alien Access[18]
- Write An Alien Microservice/Component[19]
- Write Web Component Wrapper Around Alien Service[20]
- Replace Host Service With Web Component[21]
- Rinse & Repeat For All Of Your Components[22]
- Switch To Alien[23]
1. Identify Microservices
As Part 1[24] suggests, in this step, we have to structure our application into small, independent services dedicated to one particular job. The attentive reader might notice that our to-do application is already small and independent and can represent one single microservice on its own. This is how I would treat it myself if this application would live in some broader context. Remember, however, that the process of identifying microservices is entirely subjective and there is no one correct answer.
So, in order to see the process of Frankenstein Migration in more detail, we can go a step further and split this to-do application into two independent microservices:
- An input field for adding a new item.
This service can also contain the application’s header, based purely on positioning proximity of these elements. - A list of already added items.
This service is more advanced, and together with the list itself, it also contains actions like filtering, list item’s actions, and so on.

Tip: To check whether the picked services are genuinely independent, remove HTML markup, representing each of these services. Make sure that the remaining functions still work. In our case, it should be possible to add new entries into localStorage (that this application is using as storage) from the input field without the list, while the list still renders the entries from localStorage even if the input field is missing. If your application throws errors when you remove markup for potential microservice, take a look at the “Refactor If Needed” section in Part 1[26] for an example of how to deal with such cases.
Of course, we could go on and split the second service and the listing of the items even further into independent microservices for each particular item. However, it might be too granular[27] for this example. So, for now, we conclude that our application is going to have two services; they are independent, and each of them works towards its own particular task. Hence, we have split our application into microservices.
2. Allow Host-to-Alien Access
Let me briefly remind you of what these are.
- Host
This is what our current application is called. It is written with the framework from which we’re about to move away from. In this particular case, our jQuery application. - Alien
Simply put, this one’s a gradual re-write of Host on the new framework that we are about to move to. Again, in this particular case, it’s a React or Vue application.
The rule of thumb when splitting Host and Alien is that you should be able to develop and deploy any of them without breaking the other one — at any point in time.
Keeping Host and Alien independent from each other is crucial for Frankenstein Migration. However, this makes arranging communication between the two a bit challenging. How do we allow Host access Alien without smashing the two together?
Adding Alien As A Submodule Of Your Host
Even though there are several ways to achieve the setup we need, the simplest form of organizing your project to meet this criterion is probably git submodules[28]. This is what we’re going to use in this article. I’ll leave it up to you to read carefully about how submodules in git work[29] in order to understand limitations and gotchas[30] of this structure.
The general principles of our project’s architecture with git submodules should look like this:
- Both Host and Alien are independent and are kept in separate
gitrepositories; - Host references Alien as a submodule. At this stage, Host picks a particular state (commit) of Alien and adds it as, what looks like, a subfolder in Host’s folder structure.

The process of adding a submodule is the same for any application. Teaching git submodules is beyond the scope of this article and is not directly related to Frankenstein Migration itself. So let’s just take a brief look at the possible examples.
In the snippets below, we use the React direction as an example. For any other migration direction, replace react with the name of a branch from Frankenstein TodoMVC or adjust to custom values where needed.
If you follow along using the original jQuery TodoMVC application:
$ git submodule add -b react git@gitlab.com:mishunov/frankenstein-todomvc.git react
$ git submodule update --remote
$ cd react
$ npm i
If you follow along with migration/jquery-to-react (or any other migration direction) branch from the Frankenstein Demo repository, the Alien application should already be in there as a git submodule, and you should see a respective folder. However, the folder is empty by default, and you need to update and initialize the registered submodules[32].
From the root of your project (your Host):
$ git submodule update --init
$ cd react
$ npm i
Note that in both cases we install dependencies for the Alien application, but those become sandboxed to the subfolder and won’t pollute our Host.
After adding the Alien application as a submodule of your Host, you get independent (in terms of microservices) Alien and Host applications. However, Host considers Alien a subfolder in this case, and obviously, that allows Host to access Alien without a problem.
3. Write An Alien Microservice/Component
At this step, we have to decide what microservice to migrate first and write/use it on the Alien’s side. Let’s follow the same order of services we identified in Step 1 and start with the first one: input field for adding a new item. However, before we begin, let’s agree that beyond this point, we are going to use a more favorable term component instead of microservice or service as we are moving towards the premises of frontend frameworks and the term component follows the definitions of pretty much any modern framework.
Branches of Frankenstein TodoMVC repository contain a resulting component that represents the first service “Input field for adding a new item” as a Header component:
Writing components in the framework of your choice is beyond the scope of this article and is not part of Frankenstein Migration. However, there are a couple of things to keep in mind while writing an Alien component.
Independence
First of all, the components in Alien should follow the same principle of independence, previously set up on the Host’s side: components should not depend on other components in any way.
Interoperability
Thanks to the independence of the services, most probably, components in your Host communicate in some well-established way be it a state management system, communication through some shared storage or, directly via a system of DOM events. “Interoperability” of Alien components means that they should be able to connect to the same source of communication, established by Host, to dispatch information about its state changes and listen to changes in other components. In practice, this means that if components in your Host communicate via DOM events, building your Alien component exclusively with state management in mind won’t work flawlessly for this type of migration, unfortunately.
As an example, take a look at the js/storage.js file[33] that is the primary communication channel for our jQuery components:
...
fetch: function() {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
},
save: function(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
var event = new CustomEvent("store-update", { detail: { todos } });
document.dispatchEvent(event);
},
...
Here, we use localStorage (as this example is not security-critical[34]) to store our to-do items, and once the changes to the storage get recorded, we dispatch a custom DOM event on the document element that any component can listen to.
At the same time, on the Alien’s side (let’s say React) we can set up as complex state management communication as we want. However, it’s probably smart to keep it for the future: to successfully integrate our Alien React component into Host, we have to connect to the same communication channel used by Host. In this case, it’s localStorage. To make things simple, we just copied over Host’s storage file into Alien and hooked up our components to it[35]:
import todoStorage from "../storage";
class Header extends Component {
constructor(props) {
this.state = {
todos: todoStorage.fetch()
};
}
componentDidMount() {
document.addEventListener("store-update", this.updateTodos);
}
componentWillUnmount() {
document.removeEventListener("store-update", this.updateTodos);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.todos !== this.state.todos) {
todoStorage.save(this.state.todos);
}
}
...
}
Now, our Alien components can talk the same language with Host components and vice versa.
4. Write Web Component Wrapper Around Alien Service
Even though we’re now only on the fourth step, we have achieved quite a lot:
- We’ve split our Host application into independent services which are ready to be replaced by Alien services;
- We’ve set up Host and Alien to be completely independent of each other, yet very well connected via
git submodules; - We’ve written our first Alien component using the new framework.
Now it’s time to set up a bridge between Host and Alien so that the new Alien component could function in the Host.
Reminder from Part 1[36]: Make sure that your Host has a package bundler available. In this article, we rely on Webpack, but it doesn’t mean that the technique won’t work with Rollup or any other bundler of your choice. However, I leave the mapping from Webpack to your experiments.
Naming Convention
As mentioned in the previous article, we are going to use Web Components[37] to integrate Alien into Host. On the Host’s side, we create a new file: js/frankenstein-wrappers/Header-wrapper.js. (It’s going to be our first Frankenstein wrapper.) Keep in mind that it’s a good idea to name your wrappers the same as your components in Alien application, e.g. just by adding a “-wrapper” suffix. You”ll see later on why this is a good idea, but for now, let’s agree that this means that if the Alien component is called Header.js (in React) or Header.vue (in Vue), the corresponding wrapper on the Host’s side should be called Header-wrapper.js.
In our first wrapper, we begin with the fundamental boilerplate for registering[38] a custom element[39]:
class FrankensteinWrapper extends HTMLElement {}
customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);
Next, we have to initialize Shadow DOM for this element.
Please refer to Part 1[40] to get reasoning on why we use Shadow DOM.
class FrankensteinWrapper extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
}
}
With this, we have all the essential bits of the Web Component set up, and it’s time to add our Alien component into the mix. First of all, at the beginning of our Frankenstein wrapper, we should import all the bits responsible for the Alien component’s rendering.
import React from "../../react/node_modules/react";
import ReactDOM from "../../react/node_modules/react-dom";
import HeaderApp from "../../react/src/components/Header";
...
Here we have to pause for a second. Note that we do not import Alien’s dependencies from Host’s node_modules. Everything comes from the Alien itself that sits in react/ subfolder. That is why Step 2 is so important, and it is crucial to make sure the Host has full access to assets of Alien.
Now, we can render our Alien component within Web Component’s Shadow DOM:
...
connectedCallback() {
...
ReactDOM.render( , this.shadowRoot);
}
...
Note: In this case, React doesn’t need anything else. However, to render the Vue component, you need to add a wrapping node to contain your Vue component like the following:
...
connectedCallback() {
const mountPoint = document.createElement("div");
this.attachShadow({ mode: "open" }).appendChild(mountPoint);
new Vue({
render: h => h(VueHeader)
}).$mount(mountPoint);
}
...
The reason for this is the difference in how React and Vue render components: React appends component to referenced DOM node, while Vue replaces referenced DOM node with the component. Hence, if we do .$mount(this.shadowRoot) for Vue, it essentially replaces the Shadow DOM.
That’s all we have to do to our wrapper for now. The current result for Frankenstein wrapper in both jQuery-to-React and jQuery-to-Vue migration directions can be found over here:
To sum up the mechanics of the Frankenstein wrapper:
- Create a custom element,
- Initiate Shadow DOM,
- Import everything needed for rendering an Alien component,
- Render the Alien component within the custom element’s Shadow DOM.
However, this doesn’t render our Alien in Host automatically. We have to replace the existing Host markup with our new Frankenstein wrapper.
Fasten your seatbelts, it may not be as straightforward as one would expect!
5. Replace Host Service With Web Component
Let’s go on and add our new Header-wrapper.js file to index.html and replace the existing header markup with the newly-created custom element.
...
...
Unfortunately, this won’t work as simple as that. If you open a browser and check the console, there is the Uncaught SyntaxError waiting for you. Depending on the browser and its support for ES6 modules[41], it will either be related to ES6 imports or to the way the Alien component gets rendered. Either way, we have to do something about it, but the problem and solution should be familiar and clear to most of the readers.
5.1. Update Webpack and Babel where needed
We should involve some Webpack and Babel magic before integrating our Frankenstein wrapper. Wrangling these tools is beyond the scope of the article, but you can take a look at the corresponding commits in the Frankenstein Demo repository:
Essentially, we set up the processing of the files as well as a new entry point frankenstein in Webpack’s configuration to contain everything related to Frankenstein wrappers in one place.
Once Webpack in Host knows how to process the Alien component and Web Components, we’re ready to replace Host’s markup with the new Frankenstein wrapper.
5.2. Actual Component’s Replacement
The component’s replacement should be straightforward now. In index.html of your Host, do the following:
- Replace
DOM element with
; - Add a new script
frankenstein.js. This is the new entry point in Webpack that contains everything related to Frankenstein wrappers.
...
...
That’s it! Restart your server if needed and witness the magic of the Alien component integrated into Host.
However, something still seemd to be is missing. The Alien component in the Host context doesn’t look the same way as it does in the context of the standalone Alien application. It’s simply unstyled.

Why is it so? Shouldn’t the component’s styles be integrated with the Alien component into Host automatically? I wish they would, but as in too many situations, it depends. We’re getting to the challenging part of Frankenstein Migration.
5.3. General Information On The Styling Of The Alien Component
First of all, the irony is that there is no bug in the way things work. Everything is as it’s designed to work. To explain this, let’s briefly mention different ways of styling components.
Global Styles
We all are familiar with these: global styles can be (and usually are) distributed without any particular component and get applied to the whole page. Global styles affect all DOM nodes with matching selectors.
A few examples of global styles are
