Integrating React into any existing system
You can embed react applications into any existing system using `render` and `portal` functions to render the apps into target elements. Find out how!
TL;DR: Jump to the implementation.
Summary
It is hard to re-do your entire stack at once. Using the top-level render API of react (and any other alike framework), it is easy to embed modern, new applications into an old, legacy stack without needing to re-implement the entire thing. These new applications remain programmable standalone and can be interacted with from both old code and between each other.
The key to implementing such a pattern is to use the render
and portal
functions to render the apps into target elements. Te elements in turn can be made placeable in CMS and other legacy systems using plain html-templates. An entry-point script then scans the entire page for such html-templates and starts their respective applications using portal
within.
Overall, this pattern promises easy and seamless integration of old and new and is the solution to the common legacy-stack problem.
Why even bother?
Context
I am a big fan of web applications, e.g. in react or svelte. However, most websites are not created from scratch. I experience this first-hand whenever I start a new project or talk to yet another customer.
This leads inevitably to conflict: As an experienced web-developer, I know about the advantages of using a modern stack; many developers, I've led, roll their eyes when they hear jQuery. "But where's the business-value!", I hear the client ask. Sounds familiar?
This article will not focus on "How do I convince the business to use a more modern stack" and neither will it answer "How do I migrate my legacy application to a modern stack". The answers to those are "It depends!" (For more details, contact me.)
How can I integrate a web-application into my existing software, project or stack?
Web-applications
A web-application (for our purposes) is an interactive application on a website that interacts with a backend using JavaScript (e.g. Ajax, fetch). Related are the SPA and the concept of JAM-stack. Often a web-application includes multiple functionalities, which each are encapsulated in their own app.
Web applications are rather "new" - the technology that enables web applications is fresh compared to the technology innovation lifecycles within companies, which is usually between 5-20 years. This can be seen in Google Trends.
Commercial applications
Some websites are "online-first" and their website is their product; examples include google, PayPal, twitch, AWS or Shopify. These websites frequently have complex applications maintained by different teams and in the end combined on the same web-page. Switching technologies typically requires their entire product to be re-written - and therefore they shy away.
For some websites, the website is the product
Typically, however, most commercial websites are for offline goods: They advertise and offer to buy products and services, such as consumer-goods, cars, electronics, train-tickets and flights, carpenter-, gardener- and legal services or electricity. These sites most typically use a CMS (advertisement) or web-shop (sales). This second type of website is largely more common! (If you don't believe me, try to list all "online-first" websites that come to mind - then list all airlines and car-brands you know; each of those has their own website!) This means, every new functionality must fit into their existing CMS or web shop.
For most websites, the website sells or advertises their products and services
Motivation
With the rise of web applications, their integration has become much more common. There are a couple of different ways to integrate web applications on existing websites.
Landing pages
Landing pages are standalone websites (often SPAs) - they can be guild as green-field applications in complete isolation. They are usually quickly pulled down again as the campaign they've been build for reaches its end. (More information.) Their district disadvantage is that they are, by design, not integrated into the existing website.
Re-write
Bigger companies sometimes rewrite their entire website - this is another greenfield approach. This approach takes a lot of time and resources and is frequently not viable.
Integration
Websites are composable. Living proof of these are all the SDKs out there ("paste this code-snippet to use our service on your website"). This approach is gradual, modular, flexible and scalable. These advantages, however, come with technical complexity. This article aims to take the technical complexity out of the challenge and provide an easy, but universal way to perform this integration.
Scenarios
This article describes how to integrate custom web applications (such as react-applications) into common CMS software (such as WordPress, Drupal, Ghost, AEM) and web-shop technology (such as Shopify).
There are different levels of integration we will consider. The approach we are covering allows to integrate multiple different applications into a website. Further, each application should be placeable by an editor/author (a non-technical employee who looks after the CMS content). In order to enable this scenario, we will make the web-applications available as building-blocks within the template or CMS.
Idea
Web applications start within a root-element. We place those root-elements in their desired places on the page (as a content-editor), scan the page (after page-load in the client browser) for elements marked as root-element, and they start their respective applications within.
"Make it so!"
Solution
Creating root elements
To get started, we define that all div
-elements with the attribute data-application
shall be treated as root-elements. The value of the attribute should provide all information necessary to run the desired application. At a minimum, this includes which application to run (type
), a unique ID and options (options
).
<div data-application='{"type": "todolist", "id": "maintodolist", "options": {}}'></div>
data-application
-divCreating author-placeable widgets
There are different ways to make the elements available to content-authors.
The easiest way is to enable defining custom HTML (as most CMS allow) and provide the content-authors with the code-snippet to paste.

It is usually quite easy to make these HTML-snippets available as placeable blocks - all common CMS-systems have a functionality to define custom blocks.




The image gallery shows how to do this for Drupal. For Ghost CMS, just use the "Make snippet" button.

The same is possible in WordPress (e.g. using the "code-snippets" plugin) and Adobe AEM (as per their official tutorial).
Packaging and scanning
No matter how you build your application (or maybe you don't build it at all), there will be a file that you load with the page. This file will be run on page-load. Typically, you'd include this file via the CMS-theme or together with the HTML snippet. Other options include modifying the raw HTML of the page.
This file can be used to initialise the application
import { todolistapp } from "./todolist";
// import all other apps
if (typeof window !== "undefined") {
window.addEventListener('DOMContentLoaded', (event) => {
// trigger all the following functionality from here
});
}
Scanning the page
Once the script is kicked off, it can be used to scan the page for any data-application-div
s. A document-selector searching for the data-attribute will detect all applications on the page. This results in a list of applications on the page.
const appsOnPage = Array.from(document.querySelectorAll("[data-application]")).map(el => {
const def = JSON.parse(el.dataset.application);
return ({ el, type: def.type, options: def.options ?? {}, id: def.id ?? uuidv4() });
// for production you probably want to run this callback in a try-catch
});
Running the applications
Using the list, each application can now be started. This is as straight-forward as it sounds: Loop over the list scanning the page has created and start the application in its element.
const applications = { todo: todolistapp };
appsOnPage.forEach(({ el, type, id, options }) => {
const App = applications[type];
ReactDOM.render(<App id={id} key={id} {...options} />, el);
});
While this demonstrates the basic functionality, there are a some caveats which are addressed in the following section "Concepts and solution details".
Concepts and solution details
With the above, one react-application is run for each div. It would be a lot better, if there was one react-application running on the page, rendering into each div. This would allow for shared state-management, shared theme, common context and more.
Provider
The goal is to wrap all applications into the same provider; portals enable thus behaviour. Instead of ReactDOM.render
, we use ReactDOM.createPortal
. Then we wrap the resulting array with the provider.
const applications = { todo: todolistapp };
const RootApplication = () => {
return <Provider>{appsOnPage.map(({ el, type, id, options }) => {
const App = applications[type];
return ReactDOM.createPortal(<App id={id} key={id} {...options} />, el);
});}</Provider>
}
const div = document.createElement("div");
ReactDOM.render(<RootApplication />, div)
document.body.appendChild(div);
This pattern allows all react apps to share a common state, theme and more: essentially the provider includes all setup the apps have in common.
const Provider = ({ children }) => <StoreProvider store={store}>
<ThemeProvider settings={settings}>
<SomeContext>
<GlobalStyles />
{children}
</SomeContext>
</StoreProvider>
</ThemeProvider>;
State management
Sometimes it is desirable or necessary to dynamically add or remove apps. For example, the hosting system might lazy-load parts of the page.
To enable such behaviour, instead of rendering the apps from a global variable, the apps can be rendered from react- (or Redux-) state.
const myexternalstore = new SomeStore({ initialState: appsOnPage })
const RootApplication = () => {
const [state, setState] = useState(myexternalstore.initialState); // for prodruction, use 'useReducer'
useEffect(() => {
const changeApp = ({ action, el, type, id, options }) => {
if(action === "add") {
setState([...appsOnPage, { el, type, id, options } ])
}
if(action === "remove") {
setState(state.filter(i => i.id !== id));
}
// update, upsert, ...
};
myexternalstore.addEventListener("action", changeApp);
return () => { myexternalstore.removeEventListener("action", changeApp); };
}, []);
return <Provider>{state.map(({ el, type, id, options }) => {
const App = applications[type];
return ReactDOM.createPortal(<App id={id} key={id} {...options} />, el);
});}</Provider>
}
A very simple but effective 'SomeStore' could just be an event-target (implementation). An app can be added by calling myexternalstore.dispatch({ action: "add", el: document.querySelector("#mydom"), type: "todoapp", id: "dynamicallyadded", options: {} })
.
Implementation
Below is a full implementation that uses all the parts explained above.
// in case the application-type does not exist
const fallback = (props) => (
<span {...props} data-error="Application not found" />
);
const Provider = ({ children }) => <>{children}</>; // put all your providers here
// all your available apps go here!
const availableApplications = {
TodoApp: TodoApp
// ... more application types ...
};
// All applications run from one react root
// This way they share all providers (e.g. store, intl or theme)
const RootApplication = () => {
const [state, setState] = useState(store.appsOnPage); // for prodruction, use 'useReducer'
useEffect(() => {
const changeApp = ({ action, el, apptype: type, id, options }) => {
console.log(action)
if (action === "add") {
setState([...store.appsOnPage, { el, type, id, options }]);
}
if (action === "remove") {
setState(state.filter((i) => i.id !== id));
}
// update, upsert, batch-operations, ...
};
store.addEventListener("action", changeApp);
return () => {
store.removeEventListener("action", changeApp);
};
}, []);
return (
<Provider>
{
// Provider contains store, theme, etc - see above
state.map(({ el, type, options, id }) => {
// Translate the applicationtype into the root of the application
const Application = availableApplications[type] ?? fallback;
// render the application
return (
<div key={id}>
{ReactDOM.createPortal(
<Application {...options} key={id} id={id} scope={id} />,
el
)}
</div>
);
})
}
</Provider>
);
};
// scan the page for applications
const appsOnPage = Array.from(
document.querySelectorAll("[data-application]")
).map((el) => {
const def = JSON.parse(el.dataset.application);
return {
el,
type: def.type,
options: def.options || {},
id: def.id // ?? uuidv4() // you might want to create a unique id if none existed
};
});
// define the store
const store = new EventTarget({ appsOnPage }); // see link above
// start the react root application
const div = document.createElement("div");
ReactDOM.render(<RootApplication />, div);
document.body.appendChild(div);
// in your js when you want to add/remove an app
store.dispatchEvent({ type: "action", action: hasApp ? "add" : "remove", apptype: "TodoApp", "options":{},"id":"dynamicapp", el: document.querySelector("#dynamicapp") })
// in the html:
<div data-application='{"type":"TodoApp","options":{},"id":"1234"}'></div>;
Takeaway
Evaluation
While not trivial, with the steps described in this post, it is easy to use React in the context of other systems. This enables seamless development of new applications in their modern stack without needing to give up the existing system.
Extensions
Base64-ed options
To avoid escaping and rendering issues, it makes sense to base64 the options in the data-attribute.
// js
const def = JSON.parse(atob(el.dataset.application));
// html
<div data-application='eyJ0eXBlIjoiVG9kb0FwcCIsIm9wdGlvbnMiOnt9LCJpZCI6IjEyMzQifQ=='></div>
App to app communication
Apps sometimes need to communicate with each other. They can do so, utilising their IDs and a common store. Details are described in a future blog-post.
Embedding CMS content within react
CMS often allow using "content within content". For example, if one of the react-apps is an accordion component, it is required to allow content within. It is easy to achieve this by extracting the DOM from inside the app's HTML before hydrating the react-app.
// in the html:
<div data-application='{"type":"Accordion","options":{},"id":"1234"}'>
<template data-slot="slot1">
{accordion content here}
</template>
</div>
// js
const fragment = document.createDocumentFragment();
const appsOnPage = [];
const initialize = (doc) => Array.from(
doc.querySelectorAll("[data-application]")
).forEach((el) => {
const def = JSON.parse(el.dataset.application);
const embeddedContent = Object.fromEntries(Array.from(el.querySelectorAll("template")).map(el => {
fragment.appendChild(el); // move the child out of the element
initialize(fragment); // allow app-in-app
return [el.dataset.slot, el];
}));
el.innerHTML = ""; // clean-up to avoid react warnings
appsOnPage.push({
el,
type: def.type,
options: { ...def.options, ...embeddedContent }, // slot1 is now a html-element
id: def.id // ?? uuidv4() // you might want to create a unique id if none existed
});
});
initialize(document);
// react component
function Accordion(props) {
const contentref = useRef(null);
const refreshdom = useCallback(() => {
if(contentref.current) {
contentref.current.appendChild(props.slot1); // move the component
} else {
fragment.appendChild(props.slot1);
}
}, []);
useLayoutEffect(refreshdom); // on each re-render ensure to move the elment into the div
return (
<div>
<div ref={contentref} />
other stuff
</div>
);
}
SSR
Normal SSR does not work with react portals, as React doesn't know about the DOM. However, you can achieve SSR-behaviour by replacing the inner-html of each app with the static markup of their React apps.
const app = {"type":"Accordion","options":{},"id":"1234"};
const root = React.createElement(applicaitons[app.type], {
...app.options,
id: app.id,
key: app.id,
scope: app.id,
});
// template on the server
`<div data-application='${JSON.stringify(app)}'>
<template data-slot="slot1">
${accordion content here}
</template>
${ReactDOMServer.renderToStaticMarkup(root)} // or renderToString?
</div>`
For further information on SSR in portals, see Portals and SSR.
Relation to micro-frontend architecture
Note that instead of a CMS, this can also be used to orchestrate a micro-frontend. Instead of a CMS, the apps are either static or config-driven. The principle is the same, though.
Note, that this differs from the more common approach of exporting individual widgets (e.g. jquery widgets as a library) or components (e.g. react components via module federation).
On a different note, module federation is the other common way (the most common being npm package, optionally in a mono-repo) to export each application.
Svelte and other frameworks
While the above examples are for React, they work equally well for other frameworks - essentially, swap the render and portal calls for their respective ones.
Framework | Render | Portal |
---|---|---|
React 17 | ReactDOM.render |
ReactDOM.createPortal |
React 18 | ReactDOM.createRoot + root.render |
ReactDOM.createPortal |
Svelte | Main export, e.g. new App() |
not build-in, use lib |
Angular | Use the webcomponent (e.g. <app-root> ) |
e.g. Material UI |
Vue | new Vue() |
Teleport |
Acknowledgements
Original idea and concept by my good friend Robert.