Using IFrames to Pull Off a Full Front-End Rewrite

Heidi Kasemir
Udacity Eng & Data
Published in
4 min readSep 22, 2022

--

html tags

A tale as old as time: code past a certain age starts to become difficult to maintain. The combination of outdated dependencies, constantly evolving business logic, fast hacks to get a feature in on time, new best practices mingling with legacy code and more, end up making a codebase confusing to navigate and bemusing to debug. There comes a point where refactoring would take about the same amount of effort as rewriting, especially if you need to revamp many different aspects of the system, and *especially* if the UI itself is going through a major UX redesign. This is the situation my front-end team at Udacity found ourselves in while working on a 6-year-old basic React app.

We made the decision to do a fresh rewrite of the app in NextJS, but with a fast-moving product roadmap, we couldn’t just stop writing code in the legacy app, write the whole new frontend, and then flip a switch to migrate users over all at once. We needed a way to incrementally move our product functionality to the new app while introducing as little technical debt as possible.

Enter: the iframe. An iframe is a powerful html element that allows you to embed another webpage into a view. A common use-case is embedding a map for directions in a business’ “Contact” page. We decided to create a new app and rewrite each view. After finishing a view we embed it into the old app using an iframe. By doing this, the code for each replaced view only exists in the new app, and if new features or changes need to be made, it’s all work that can happen in the fresh codebase!

Wirefame representation of two applications showing a view in one to be embedded as an iframe in the other.
We are using two applications, embedding the fresh app into the legacy app as an iframe.

The Challenge

The main challenge in using iframes to replace full views in a Single Page Application (SPA) is maintaining good performance and seamless interactions — we don’t want it to be obvious that the user is actually navigating two different applications. To this end, we need to avoid unmounting and remounting the iframe or directly changing its src value. Doing this during parent-to-child navigation changes would cause a full refresh of the embedded view rather than taking advantage of our SPA’s client-side routing benefits. Same goes for the child-to-parent link/redirect behavior — given a link in the embedded view that should point the parent to a new location, we need to use client-side routing.

Setting up Two-Way Communication

To enable the parent window and its child iframe to talk to each other, we used window.postMessage and added event listeners to listen for ‘message’ events.

The above code snippet shows a solution for both apps using React hooks to set up listeners that will handle “message” events coming from either the parent window or the child iframe. (Note that it’s a good idea to check that the message is coming from a trusted origin before doing anything with it.) The handlers use the data passed along in the message to perform a client side navigation action. By setting up these listeners and helpers to post messages, we can now start handling all the locations where a link in one app would actually route to a path in the other.

Child to Parent vs Local Navigation

Getting the parent window to update from within the iframe is pretty straightforward with the above utilities in place. As we develop the replacement app, though, we need to handle cases when the user is directly navigating the new app (not in an iframe) as well. To detect when the user is viewing our new app within an embedded iframe, we append a query param to the url (for example, ?isEmbedded=true) and implement a custom hook to identify if this query param is present. We created an IframeAwareLink component that identifies when the app is viewed as embedded in an iframe, and overrides the navigation action to post a message to the parent, but otherwise will navigate locally as expected.

Rendering the Correct Route in the Iframe

To take advantage of performance gains of client-side routing in the iframe, we set up an “active” and “hidden” state so we never need to unmount the iframe, which would cause a refresh. When “active”, it takes up the whole screen, and when “hidden”, it takes up a single pixel, which disappears in the content of the page. We use the parent url as the source of truth for if the iframe should be “active”, and what path should be shown in the iframe.

The helper named “getIframeRoute” above takes the parent url, and matches it to a replaced view if there is one available. If there is no match, it means that the view hasn’t been rewritten yet and we still need to display the legacy view. In this case, the iframe shrinks down, renders a blank route, and sits invisible until it becomes active again on a route that has been rewritten.

Conclusion

If you ever need to rewrite your app and want to start fresh in a new codebase, but can’t just pause development on your current product, then using iframes to incrementally replace views could be a great solution. They can be set up to relatively seamlessly integrate with client-side routing strategies to maintain good performance, and won’t block development in either the old or new applications.

--

--

I am a Senior Software Engineer at Udacity. I geek out about React, front end testing, typescript, and rock climbing.