Screenshot from the Backbone with React-modules PoC

With a huge monolithic frontend application that is getting on in years, you will come to a point where the cost of maintenance and incremental changes is considerable, radical changes seems impossible, and recruiting for an outdated technology is a major hassle. It is time for a rewrite!

There are different strategies for starting on that daunting process. But there might be some situations where the better option is a modular rewrite that mixes new into the old. This strategy will never be optimal, since you will be adding significant overhead by using two frameworks. But just let's say the total tradeoff can still come in favour of this strategy, and move on to the technical practicalities. Here I will describe a Proof of Concept of how it can be done. It mixes React in Backbone, since that was our use case, but the principles should be relevant for other frameworks as well.

The PoC

The PoC is based on the the Backbone ToDo app (https://backbonejs.org/docs/todos.html), vamped up with some Material design.

To this I have added two React modules. One that is an alternative to the Backbone List-view (it is still there, hidden by a checkbox, to show that they are synched), and one that adds features to the editing of the todo. They both uses Material UI (https://material-ui.com/) so that the styles can be aligned with the Backbone app. These two modules are compiled and deployed independently of each other, but they still share mui-theme and Redux-store.

Demo: https://custom-react-elements.now.sh/

GitHub: https://github.com/kjartanm/custom-react-elements

Here are some concerns that the PoC addresses:

It should not add unnecessary complexity to the legacy application.

It should not add unnecessary overhead to the legacy application.

It should be possible to share state between different React modules.

It should be possible to easily communicate state between React modules and legacy application.

These concerns are addressed through the following steps:

Step 1: Deploy and integrate React modules as custom-elements

There are different ways to mix React into a legacy framework. The PoC use a technique where the React module is compiled into a custom element. A ‘hello world’ example of this technique would look something like this:

const HelloWorldComponent = () => {

return (

<h1>Hello world!</h1>

)

} class HelloWorldElement extends HTMLElement {

connectedCallback() {

ReactDOM.render(HelloWorldComponent(), this);

}

}

customElements.define('hello-world', HelloWorldElement); //Then you can just use the custom element to render and add the React component to the DOM <div class="legacyapp-container">

<hello-world></hello-world>

</div>

The benefit of this technique is that you can add the React-modules with minimal impact on the codebase of the legacy app. Instead, you do it indirectly through the legacy frameworks template handling. You can then focus on removing code from the legacy app, replacing full logic components with dumb components just for presenting the templates with the custom elements.

Step 2: Share resources between React modules

You would not like to bundle React and Redux etc. for every component. Fortunately its quite easy to set up those kinds of dependencies as shared, external dependencies while compiling the modules. The PoC loads React & Co from CDN, and the rollup config (but this can probably be done as easy with webpack) tells the script where to find them:

//from rollup.config.js



const globals = {

react: 'React',

redux: 'Redux',

'react-dom': 'ReactDOM',

'react-theme': 'reactTheme',

'react-store': 'reactStore',

} const externals = ['react', 'redux', 'react-dom', 'react-theme', 'react-store'] export default [

{

input: 'src/Module1Wrapper.js',

output: {

file: 'public/module1.js',

format: 'iife',

globals,

name: 'Module1'

},

externals,

plugins

},

...

]

Local dependencies can be handled in the same way. The shared theme is exported as an iife (immediately invoked function expression) that sets the export as a named global that can be used as a shared dependency:

//from rollup.config.js {

input: 'src/theme.js',

output: {

file: 'public/theme.js',

format: 'iife',

globals,

name: 'reactTheme'

},

external,

plugins

}

If a resource should be shared or not, is not always traight forward. In the PoC I could have loaded the Material UI-library through a CDN and treated it the same way as with React, Redux. But the overhead of loading the whole library outweighs the benefit. Instead, if I really needed to optimize, I could create a local bundle of just the needed components, and served those in the same way as the theme.

Step 3: Share state between React modules

You can share a redux store between modules the same way as the theme:

//from rollup.config.js {

input: 'src/store.js',

output: {

file: 'public/store.js',

format: 'iife',

globals,

name: 'reactStore'

},

external,

plugins

},

The React modules will then both ‘import’ the global store, making it available for the module together with the theme:

const App = () => {

return (

<Provider store={reactStore}>

<ThemeProvider theme={reactTheme}>

<SimpleEdit></SimpleEdit>

</ThemeProvider>

</Provider>

)

}

Since the shared resource is a created and ready store, the state will be shared between the modules.

Step 4: Communicate state between React modules and legacy app

The nice thing with the shared Redux store is that it also can be used to communicate state between legacy app and React modules. Here we need to add something to the legacy app, but with some care that can be done centrally, and not dispersed throughout the app. In the PoC, for example, I have set up a listener on the Todos-collection in the main script for dispatching todos to the modules:

Todos.on("change destroy", ()=>{

window.reactStore.dispatch({ type: 'SET_TODOS', payload: Todos.toJSON() })

})

And this goes the other way as well:

window.reactStore.subscribe(() => {

const state = window.reactStore.getState();

const items = (state.items?state.items:[]);

Todos.set(items);

Todos.each(todo=>todo.save())

})

This last one is rather dumb. You will soon need some abstraction that lets collections and models listen to just a slice of the state, and not the whole state since that could create a lot of noise.

There is also some danger of endless looping, since model and state are listening to each other, but as long as the incoming state can be checked off as equivalent with existing, it will stop. If not, you will need some bookkeeping to make sure. But all that is out of the PoC scope, and something for another day.

Conclusion

The PoC works, but there is, of course, a difference between creating a PoC and using this strategy in a full-blown legacy application. There will be hick ups, problems with synhcing, and a significant addition to the overhead — even if it can be contained. If this is time well spent, is an open question, and the only answer is that it depends.