About The Author

Denys is a frontend developer & public speaker. Being 2-in-1: an art school graduate and an engineer, Denys is passionate about psychology, physics, …
More about
Denys
Mishunov

We recently discussed what “Frankenstein Migration” is, compared it to conventional types of migrations, and mentioned two main building blocks: microservices and Web Components. We also got a theoretical basis of how this type of migration works. If you didn’t read or forgot that discussion, you might want to get back to Part 1 first because it helps to understand everything we’ll cover in this second part of the article.

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. 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.

It’s time to put the theory to the test
It’s time to put the theory to the test. (Large preview)

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.

A default view of a TodoMVC application
A default view of a TodoMVC application (Large preview)

For this article, as a starting point, I picked a jQuery application from the TodoMVC project — 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 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.
Our goals: results of the migration to React and Vue
Our goals: results of the migration to React and Vue. (Large preview)

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

    This repository contains TodoMVC applications in different frameworks/libraries. For example, you can find branches like vue, angularjs, react and jquery in this repository.
  • Frankenstein Demo

    It contains several branches, each of which represents a particular migration direction between applications, available in the first repository. There are branches like migration/jquery-to-react and migration/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 jquery branch from the Frankenstein TodoMVC repository and strictly follow all of the instructions below.
  • Alternatively, you can open a branch dedicated to either migration to React or migration to Vue from the Frankenstein Demo 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 of the article.

Let’s dive right in!

  1. Identify Microservices
  2. Allow Host-to-Alien Access
  3. Write An Alien Microservice/Component
  4. Write Web Component Wrapper Around Alien Service
  5. Replace Host Service With Web Component
  6. Rinse & Repeat For All Of Your Components
  7. Switch To Alien

1. Identify Microservices

As Part 1 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:

  1. 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.
  2. 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.
TodoMVC application split into two independent microservices
TodoMVC application split into two independent microservices. (Large preview)

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 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 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. 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 in order to understand limitations and gotchas 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 git repositories;
  • 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.
React TodoMVC added as a git submodule into jQuery TodoMVC application
React TodoMVC added as a git submodule into jQuery TodoMVC application. (Large preview)

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.

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 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) 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:

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: 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 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 a custom element:

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 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:

  1. Create a custom element,
  2. Initiate Shadow DOM,
  3. Import everything needed for rendering an Alien component,
  4. 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, 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:

  1. Replace
    DOM element with ;
  2. 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.

Unstyled Alien React component after being integrated into Host
Unstyled Alien React component after being integrated into Host (Large preview)

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