Embedding React into legacy web pages

The product I am enthusiastically working on - Navigo3 - was historically written as combination of XSLT and JSP. Do you remember XSLT?

meme

Short time before I joined it it was decided that is should be remade in React.js. It was great decision but transition is not that easy.

Because Navigo3 is in production usage we have to transform from XSLT to React step by step. Some pages are that complex that it would be handy being able to combine XSLT and React on single page. Typically it is required on pages that combine many types of information - dashboards. A few graphs, tables, list of tasks, … It would be hard to rewrite all at glance. But rewrite each one is quite simple.

dashboard.png

At the same time we have new pages which are written completely in React and uses hashtag navigation. In main entry file index.js there is redux-router hierarchy and it expects that it runs on purely React page. This actually does not match with demand to embed some components into JSP/XSLT static page with classical URL navigation, right?

But I would like to share React components between brand new react pages and legacy XSLT pages! And at some moment just compose React page from existing components and abandon legacy ones. So how to do that?

  1. Prepare second entry file (like indexLegacy.js) and prepare build task for it (we use Webpack).
  2. Insert result bundle file via into JSP/XSLT pages right before </body> element.
  3. In JSP/XSLT/whatever page put empty DIV tag in place where React components should be rendered and give it nice id or class name - like “hack_legacy_tasks_list”.
  4. In new entry file write logic that basically perform list of IFs that fills prepared DIVs - but only if they exists:
    • Check if desired element with unique name is presented in DOM (we use jQuery for that)
    • If it is presented render desired component into it (using React.render(, element))
  5. Of course list of IFs can be replaced by some metadata and have generic code that process them.
  6. If component needs some context, you can wrap it. But you have to do it for all of them separately. They do not share tree hierarchy (React instance).
  7. If all components on page needs to share something, it can be instantiated in entry file and passed to all components.

That’s it. Lets sum up some advantages:

  • Components are shared between new shiny React pages and legacy pages.
  • There is single entry bundle for legacy stuff. If your legacy pages includes shared footer file you can place reference to legacy bundle there. Because unique ids/classnames are used for binding, it should not harm any existing content.
  • You may place multiple components per legacy page - insert multiple DIVs.

React.js FTW!

Here is sample of indexLegacy.js

//libraries 
import React from "react" 
import {render} from "react-dom" 
import moment from "moment" 
import jQuery from 'jquery' 
import {Utils} from './utils/Utils' 

//container for legacy components 
import {LegacyApp} from './containers/LegacyApp' 
import {ReactDemo} from './quark/ReactDemo' 

//...and other 10+ components 

//key is CSS selector, value is function that returns React element 
const instances = { 
  '.hack_react_demo': (elem)=><ReactDemo/> //this defines method that creates React component for HTML element <div class='hack_react_demo'/> placed everywhere 
  //...and other 10+ components 
} 

//render reactElement into DOM elemenet wrapped by LegacyApp function 
__createReactInstance(element, reactElement) { 
  render(<LegacyApp>{reactElement}</LegacyApp>, element) 
} 
  
//for each entry in instances object 
Object.keys(instances).forEach(selector=>{ 
  //for each DOM element found by jQuery 
  jQuery(selector).each((i, element)=>{ 
    //get function for selector and call it (passing DOM element) 
    const reactElement = instances[selector](element) 
    
    //create React component 
    __createReactInstance(element, reactElement) 
  }) 
})

ReactDemo.js:

import React, { Component } from 'react'
import PropTypes from 'prop-types'

export class ReactDemo extends Component  {
  render() {
    return <div>
      Hi, Stereo!
    </div>
  }
}

LegacyApp.js:

import React, {Component} from "react";
import PropTypes from 'prop-types'

export class LegacyApp extends Component {

  static childContextTypes = {
    //define some context for child pages - app settings, etc.
  }

  getChildContext() {
    return {
      //place context here
    };
  }

  render() {
    return <div>
      {this.props.children}
    </div>
  }
}

Fragment of JSP page:

<div class='hack_react_demo'></div>
Tags:  React