Not so long ago, that I encountered an interesting application refactoring challenge in one of the legacy web apps.
And when I’m talking about legacy web apps, I’m usually referring to server-side rendering web applications.
In this legacy web app, I have the grand opportunity to design and refactor web UI controls to be heavily built in React alongside with leveraging client-side routing such as React Router.
But due to the sheer size of its monolithic complexity behind such web forms, the time required to completely revamp the UI interface could not be accomplished within our client’s timeline budget of delivery. We need to ship some parts of the system that’s already remade in React in the first phase, but the rest of the legacy systems would have to hop aboard with the rest of the React app with it.
Thus the solution to serve the other legacy system menus and app access within React is simply to make use <iframe>
tag.
Let’s say for eg you have the following React Router (at the time of writing, I’m using version 4) component definition:
import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
const ReactApp = () => (
<BrowserRouter>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/about/" component={About} />
<Route path="/users/" component={Users} />
<Route path="/users/:id/profile" component={UserProfile} />
</Switch>
</BrowserRouter>
);
Let’s say that you managed to have Home
and About
React components designed and implemented, but somehow you are nowhere near confident in implementing the Users
and UserProfile
pages to match the legacy environments, just yet.
Thus you need to ‘house’ these legacy apps under React when user navigate such pages in the interim.
Therefore our solution (as originally proposed above) would have to look something like these.
const Users = () => (
<Fragment>
<iframe src="some_legacy_url/users" title="some legacy app title" />
</Fragment>
);
const UserProfile = ({id}) => (
<Fragment>
<iframe src={`some_legacy_url/user/${id}/profile`} title="some legacy app title" />
</Fragment>
);
With the iframe
tags and page components defined above, I know the legacy URLs to the Users
and UserProfile
pages are going to be needed here beforehand. When users navigate to these pages within React, what’s going to happen behind the scenes is that React will render the iframe
upon load, the iframe
takes over the responsibility of pre-fetching all of its resources from the web server that hosted the legacy web apps and loads their respective mapped pages.
Once that’s complete, you can immediately see their actual legacy pages and ready to be used, just like you would if you were normally using them in the legacy environment!
How is it possible?
Well.
You can think of iframe
as an URL browser that lets you navigate pages as well, with the only difference that the same document page is embedded within the main HTML document window. Which in this case, it’s the React’s HTML document window. You can see a working example from this link from W3Schools so you see what I mean!
That’s fantastic!
We got the client-routing to server-side-rendering template mapping problems solved!
However, at this point, I’ve managed to solve only one aspect of this problem.
Sure - the user can enter the browser’s URL as http://some_react_base/users
and http://some_react_base/user/${id}/profile
to land the correct page resources.
Notice there’s another aspect of the problem here - the some_legacy_url/users
page contains a list of users that have the hyperlink to their profile information ie some_legacy_url/user/:some_id/profile
. Now you would think that by clicking one of those user profiles would trigger React Router to load and server the page on some_react_base_url/user/:same_id/profile
?
Well.. It doesn’t really.
What actually happened here is that iframe
will no doubt load server-side-rendering UserProfile
template within React app properly. The browser URL routing navigation, however, does not. This is because React has no control over the iframe
resources as soon as Users
legacy page is loaded. For any server-side-rendering applications, they have their own internal page routing that leads the user to navigate other server-side rendering pages such as Userprofile
. Therefore as soon as the user starts navigating to UserProfile
page, the main browser window does not leave http:/some-react-base-url/users
. Instead, it is the iframe
leaves the users URL for another page. This is because as I mentioned earlier in this post, iframe
itself is treated like an URL browser window on its own.
That’s why React has no ‘clue’ what iframe
navigation activities are happening over there.
Okay.. That’s good to know.
However, for our case, we do want our React to have control over this navigation behaviour as we may wish to use our menu and page navigation layouts that are different to the legacy’s menus and page navigation setup. Thus we just want to mirror our client-side-routing to the server-side routing paths respectively (and interchangeably).
So - how should we accomplish this?
Simple.
We use the browsers’ DOM API to do this.
Supposed we have the following Python Flask’s Users template
<html>
<title>Users Page</title
<body>
<h1>List of Users</h1>
<table id="users_table">
<tbody>
{% for user in users %}
<tr>
<td>{{user.name}}</td>
<td>{{user.email}}</td>
<td><a href=/user/{{user.id}}/profile>Profile detail</></td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
We have a table rendered with a list of users’ information and each row of a user has a hyperlink that contains their profile in detail.
Next, we hook up our jQuery to manage the individual hyperlink.
$(document).ready( function() {
//grab the column elements that has hyperlinks
var user_profile_links = $("table[id='users_table'] tr td:nth-child(3)")
//bind click event for each profile link
user_profile_links.each( function(index) {
$(this).on("click", "a" , function(e) {
e.preventDefault();
var user_profile_url = $(this).attr('href')
document.location.href = user_profile_url;
})
});
}
Nothing new here. Very basic stuff.
Somewhere in our server-side legacy code base, we handle user profile routing over there.
But here’s the real kicker…
Instead of using document.location.href
, we use the window’s frameElement
.
var isEmbeddedInFrame = window.frameElement;
...
//put this inside each table's column onclick callback
if(isEmbededInFrame) {
var topParentWindow = isEmbeddedInFrame.ownerDocument.defaultView;
topParentWindow.history.pushState({}, '', user_profile_url);
topParentWindow.history.go();
}
Confused? What’s going here, you’re probably wondering..?
How do these help React Router to route the UserProfile pages exactly?
I’m glad that you asked.
Let me explain.
When verifying the template is definitely sitting inside the iframe element by calling window.frameElement API
call, we’re now granted the access to all of its DOM properties and API method calls just like you would with document
and window
object in the browser.
What’s more interesting here is in newer browser versions of Chrome, Safari etc, we can reach out to the main parent window that embeds the current iframe we’re in by calling ownerDocument.defaultView
, which returns us, essentially, another window
object.
And just like any window
object, we can access any of its HTML5 APIs at our disposal such as history
.
Here, our topParentWindow’s history pushState
method call is to create a new URL entry and store it to our browser’s URL history. Not only that, the URL entry will be displayed in the URL link as well. It takes 3 parameters
- State object - The state object is a JavaScript object which is associated with the new history entry created by
pushState
. - title - using this will display the URL title of the current tab you’re navigating to.
- URL - lastly (and most importantly), the URL entry you want the user to be redirected to.
Using pushState
does not trigger the new URL entry to reload the page. It’s not responsible for handling that. You need to execute its go
method to make this happen.
Once you do this, the magic happens when the user_profile_url
link is loaded in the main browser, React Router routing mechanism will start to kick in, and match user_profile_url
link to one of its URL routing rules - which is
<Route path="/users/:id/profile" component={UserProfile} />
Assuming they are matched correctly, it will load the UserProfile
component correctly along with its own iframe
resources pertaining to the user profile information - which will be rendered by the same legacy app.
Thus, it gives the user the illusion that the user is navigating the URL pages within the same environment, but really, both React and legacy app are just getting synced up with one another.
And there you have!
That’s how you can get both client-side routing and server-side working side by side when hosting legacy apps within React environment!!
The reason why this works is because underneath the React Router library, its core engine fundamentally makes full use of HTML5 history API just like the pushState I mentioned earlier, if you read its user docs in detail especially if you’re planning to use BrowserRouter
component for the majority of client-routing experience for all of your React page components.
This is the perfect solution for me in the interim before I can plan for more time in redesigning and, eventually, implementing the new User
and UserProfile
pages in React, thus allowing me to do legacy-to-React app migration gracefully.
Then you may ask sure this is great stuff to know! But what if you’re still serving the legacy web app domain in the production environment and not completely ready to shut the legacy domain completely yet? What if you want to have both the legacy and React environments running side by side? Would the code sample I wrote will break the legacy routing functionality while React is running it side by side?
Again, the solution is not too hard to implement.
We just pass in the followining conditional checks.
$(document).ready( function() {
// React will pass the following query string parameters in Flask/Python
var environment_type = "{{request.args.get('environment_type'}}";
var user_profile_links = $("table[id='users_table'] tr td:nth-child(3)")
user_profile_links.each( function(index) {
$(this).on("click", "a" , function(e) {
e.preventDefault();
var user_profile_url = $(this).attr('href');
// if the legacy app is hosted in the react environment.
if(environment_type === 'react'){
var isEmbeddedInFrame = window.frameElement;
// check if it's definitely sitting in the iframe
if(isEmbededInFrame) {
var topParentWindow = isEmbeddedInFrame.ownerDocument.defaultView;
// react router will handle the rest
topParentWindow.history.pushState({}, '', user_profile_url);
topParentWindow.history.go();
}
}
else {
// otherwise let the server-side routing take care of it.
document.location.href = user_profile_url;
}
})
});
}
Then in our Users
component, we modify to do the following:
const qs_param = '?environment_type=react';
const Users = () => (
<Fragment>
<iframe src={`some_legacy_url/users${qs_param}`} title="some legacy app title" />
</Fragment>
);
Now you have both server-side routing and client-side routing working in tandum without breaking any user navigation flows.
One last thing I want to mention here is that using iframe has security implications behind it so be sure to consult Mozilla Web API docs for handling cross-domain security resources as iframe suffered its notoriety for injecting malicious scripts as well as other possible attack vectors it comes with so you need to plan ahead how to handle the security requirements in using this for your web app environment, when needed. Hence, the need for wiring security applications is out of this post’s scope.
Hopefully, you have learnt some useful tricks from this.
Till next time, Happy Coding!
Useful References: