Greetings, all! Have you ever been asked by your business to accelerate development by incorporating third-party services? And doesn’t the idea of being able to modify design and content on your own, without having to involve a developer, sound enticing?
Currently, there are various tools available to facilitate effective collaboration among different team members, including developers, designers, and content managers. Tilda, Wordpress, and other similar solutions offer convenient means for working on different aspects of a project. However, integrating the outcomes of these tools into a React application can pose challenges. The optimal solution would be to enable interaction between our application and the content, allowing for event interception to achieve seamless integration and flexibility.
In this article, I will demonstrate how to solve three integration-related tasks in a React application:
These approaches will allow us to ensure flexibility and accessibility of our application while significantly speeding up the development process. Let’s take a closer look at each of these tasks and identify the optimal solutions.
This article will showcase different integration techniques that can save time for developers and offer greater flexibility to designers and content managers.
Typically, we expect the following from third-party solutions:
In the examples, we will address the following questions:
It is worth noting that I will provide examples of solutions using specific products, such as Tilda, Hubspot, Typeform, Contentful,
If our goal is only to offload data management to an admin interface while still wanting to create other components of the application “the old way” (by writing code and designing in an editor), then using a Headless CMS can be an optimal solution. This approach allows storing data in SaaS solutions, retrieving it through SDKs or directly, and then rendering it on the client-side using our own efforts.
Examples of such programs are Strapi and Contentful. In this article, we will explore the integration approach using the latter. One of the main tasks of this approach is to solve the problem of hardcoding data into the markup. When business requirements change, you need to dive into the code and perform a deployment, which can take a lot of time.
In the Headless CMS paradigm, we first set up the data model. For example, we can have a model called “Article” with fields like “Title” and “Article Text.” This stage can be done by developers. Then, content managers can create specific instances of entities based on this model, such as “Article 1,” “Article 2,” and so on.
Next, we will look at examples of implementing this approach to demonstrate how Contentful can help solve the problem of data hardcoding and make the process of updating and modifying content more convenient and efficient for the team.
In the case of Contentful, we can retrieve data through the standard Web API using the fetch
function. After adding access keys, we need to make a request to the Contentful API and save the received data locally for further use:
const [page, setPage] = useState(null);
useEffect(() => {
window
.fetch(
`https://graphql.contentful.com/content/v1/spaces/${process.env.REACT_APP_CONTENTFUL_SPACE}/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
// Authenticate the request
Authorization: `Bearer ${process.env.REACT_APP_CONTENTFUL_API_KEY}`,
},
// send the GraphQL query
body: JSON.stringify({ query }),
}
)
.then((response) => response.json())
.then(({ data, errors }) => {
if (errors) {
console.error(errors);
}
// rerender the entire component with new data
setPage(data.blogPostCollection.items[0]);
});
}, []);
After obtaining the data from Contentful and storing it locally, further handling of the data becomes familiar and convenient in React components. We can use the data in components just like any other data and render it on the page using JSX:
return (
<div className={"bg-white rounded-2xl p-6"}>
<h1 className={"text-6xl text-gray-900"}>{page.title}</h1>
{page.body.json.content.map((item) => (
<p className={"text-2xl m-5 p-2 text-gray-800"}>
{item.content[0].value}
</p>
))}
</div>
);
Interaction with buttons and event handling in a React application using data from Contentful follows the familiar React pattern. We can easily add a button and pass an onClick handler to the component to respond to user actions.
The process is smooth here as well. During server-side content rendering, we can make requests to Contentful and send the client pre-rendered data. If we are using Next.js, we can fetch data using getServerSideProps
(for Server Side Rendering) or getStaticProps
(for Static Site Generation). This allows us to provide users with fast and optimized content based on data retrieved from Contentful.
In this aspect, everything works smoothly as well. Data is fetched every time there is a request, eliminating the need for redeploying the application. The only consideration is if you are using the Static Site Generation approach, you’ll need to utilize
Pros:
Cons:
For a more in-depth look at integrating React and Contentful, you can read the details
In conclusion, the approach using Headless CMS, such as Contentful, provides efficient content management, saves developers’ time, and enables quick content updates on the website. However, it is essential to consider some drawbacks, such as managing multiple fields and the need for manual coding, which may require additional efforts and attention when adopting this approach.
When we have a service that can be displayed within an iframe
, and it does not have security headers like x-frame-options: SAMEORIGIN
or Content-Security-Policy: frame-ancestors none
, we can embed the content of this service on our website by displaying it inside an iframe
. Some services, such as Typeform, offer integration through iframe
and provide SDKs for easy integration. However, for websites without a ready-made SDK, if we want to add interactivity and interaction with the parent application, we need to use the postMessage
mechanism.
Let’s consider a situation where we already have an SDK for integrating another web resource into our React application. Using Typeform as an example, which provides an SDK for seamless integration, we’ll explore this approach.
Typeform offers various display options on websites, such as a slider, popup, and more. The main thing we need to do is install the @typeform/embed-react
package and add the corresponding widget to the page:
<Widget
id="iUesUyX7"
style={{ width: "100%" }}
className="my-form"
onQuestionChanged={() => {
alert("onQuestionChanged");
}}
/>
To influence the content inside the Typeform, we can use the
If we need to respond to events occurring in Typeform, we can pass event handlers onSubmit
and onReady
to the Widget
, just like we do with regular React components. However, it's essential to note that there are some limitations when working with the Typeform SDK, and we cannot directly add custom components to the form.
We face limitations when using SSR, which makes it impossible to index content displayed through an iframe
. This limitation applies to any solution using this technology.
Please be aware that the lack of Server Side Rendering support may impact your website’s SEO, as content loaded through an iframe
is inaccessible to search engine crawlers and search engines.
As we retrieve up-to-date content in the iframe
every time there's a request, we are only responsible for displaying the tag. Content updates occur on the side of the third-party service, which allows us to automatically receive the latest information without any intervention on our part. This makes it easier to maintain the freshness of content on our website, as changes in the source are automatically reflected in the iframe
.
Pros:
If a product supports integration, working through iframe
allows solving business tasks with just a few lines of code.
Content changes on the page are automatically reflected in the iframe
.
Cons:
If we want to display a website inside an iframe
and interact with it from our React application, we'll need to use postMessage
. It's essential to note that for this approach, we should have the ability to add custom HTML to the page, preferably within the <head>
.
The postMessage
mechanism allows us to establish secure communication between the parent and embedded iframe
application. With postMessage
, we can send messages and pass data between the parent and the embedded window, even if they are on different domains.
We will add code to handle requests from the parent application and code to send events from the Hubspot page. Since Hubspot does not provide an official SDK for integration, we have to use an alternative approach. Although this method is not official, it is one of the available options for interacting with Hubspot through an iframe
.
From the React application, we will add a link to Hubspot within an iframe
and pass a frameRef
, through which we'll interact with the service:
<iframe
title={"hubspot"}
src={HUBSPOT_URL}
className={"w-full h-full rounded-2xl"}
ref={frameRef}
/>
In the following code example, we have implemented the handling of links and individual buttons in Hubspot, which we defined in hubspotConfig
. This allows us to intercept and process events from the service's side. Additionally, we can add configuration to control the behavior of the Hubspot page, such as enabling or disabling specific functionalities:
const hubspotConfig = useMemo(
() => ({
handlers: [
{
type: "click-button",
name: CTA_BUTTON_NAME,
selector: "a[href='#custom']",
},
],
}),
[]
);
Then, we will subscribe to the message
event from the iframe
inside a useEffect
hook:
useEffect(() => {
const handler = (event) => {
if (!event.origin.includes("hubspot") || !event.data.type) { // #1
return;
}
switch (event.data.type) {
case "click-button": { // #2
if (
event.data.payload.buttonName === "link" &&
event.data.payload.href
) {
window.location.href = event.data.payload.href;
break;
}
alert(`Button: ${event.data.payload.buttonName}`);
break;
}
case "load": {
if (hubspotConfig && frameRef.current?.contentWindow) {
frameRef.current.contentWindow.postMessage(
createAction("set-config", { config: hubspotConfig }), // #3
"*"
);
}
break;
}
default: {
console.log("There is not event type");
}
}
};
window.addEventListener("message", handler, false);
return () => {
window.removeEventListener("message", handler);
};
}, [...])
Some interesting points to note:
origin
field, using the condition event.origin.includes("hubspot")
. This is crucial because the message
event can come from various iframe
and workers
, and we need to process only those events originating from Hubspot.click-button
generated on the Hubspot side, we handle the click in React. This way, we can react to actions happening inside the iframe
and synchronize them with the React application.set-config
action only after the iframe
has loaded (action type load
- details below). This allows us to inform Hubspot about which buttons we want to handle additionally.
In the Hubspot admin for the page, we add the following script to the head
section:
(function () {
const createAction = (type, payload) => {
return {
type,
payload,
};
};
const sendMessage = (action) => {
window.parent.postMessage(action, "*");
};
window.addEventListener("DOMContentLoaded", () => { // #1
window.addEventListener( // #2
"message",
(event) => {
if (event.data.type === "set-config") { // #4
const config = event.data.payload.config;
config.handlers?.forEach((handler) => {
const element = document.querySelector(handler.selector);
switch (handler.type) {
case "click-button": {
element?.addEventListener("click", (e) => {
e.preventDefault();
sendMessage(
createAction("click-button", {
buttonName: handler.name,
})
);
});
break;
}
default: {
// eslint-disable-next-line no-console
console.log("There is not the event: " + handler.type);
}
}
});
const links = document.querySelectorAll("a");
links.forEach((link) => {
link.onclick = "event.preventDefault()";
link.addEventListener("click", (e) => {
e.preventDefault();
sendMessage(
createAction("click-button", {
buttonName: "link",
href: link.href,
})
);
});
});
}
},
false
);
sendMessage(
createAction("load", { // #3
title: document.title,
description:
document.querySelector('meta[name="description"]')?.content || "",
})
);
});
})();
The sequence of actions is as follows:
iframe
is loaded, and the DOMContentLoaded
event is triggered.message
event.action
with type load
to notify the React application that this script is ready to receive config
. Here, we can also pass metadata about the page to add them to the React application's <head>
section for SEO purposes.set-config
action, we iterate through all the links and buttons specified in the config
and add click
handlers to them. Now, when these elements are clicked, we send information about the event to the parent application (React).
This process allows for seamless communication and interaction between the embedded Hubspot page (inside the iframe
) and the parent React application. It enables React to handle events triggered within the Hubspot page and to synchronize actions between the two environments effectively.
Using iframe
poses challenges with Server Side Rendering (SSR), and the content inside the iframe
cannot be indexed by search engines.
The reason is that the content inside the iframe
is dynamically loaded on the client-side, not on the server. As a result, when a search engine crawls the page, it cannot see the content within the iframe
because it is not available in the HTML markup that is delivered from the server.
This limitation can have a negative impact on SEO and affect content indexing, making the use of iframe
unsuitable for applications in terms of accessibility and search engine optimization.
Similar to the example with Typeform, in this case, it is also impossible to predict what may be loaded inside the iframe
, as the content can change. The content within the iframe
will always be up-to-date, and we need to interact with it based on the information received through postMessage
and the shared config
.
However, it’s essential to note that you should update the “contract” between the parent React application and the iframe
in a timely manner. If there are changes inside the iframe
that can affect the message exchange or the usage of the shared config
, you need to ensure synchronization and appropriate updates in both applications. This guarantees the correct functioning of the integration and the preservation of up-to-date information.
Pros:
Cons:
Lack of SSR support, which can have a negative impact on SEO.
Need to add a code wrapper for message exchange via postMessage
. This approach may be challenging for individuals unfamiliar with this functionality and can cause difficulties during integration and debugging.
The approach of using iframe
and postMessage
has certain advantages and limitations, and the choice of this approach depends on the project's requirements and the experience of the developers who will be working with it.
Sometimes, there are situations where we have a ready-made design exported from another website, and we want to integrate it into our React application. However, using iframe
or a proxy to interact with the third-party service is not suitable in this case.
If our site has x-frame-options: SAMEORIGIN|DENY
or Content-Security-Policy: frame-ancestors none
in the HTTP headers, it will be impossible to display it inside an iframe
. In such cases, we need to approach the task differently - by copying the markup, including all styles and scripts, and then attempt to insert it into our React application.
To demonstrate this method of integration, we will use the platform Tilda. This service allows us to export files that can be hosted on our own server. It is essential to note that officially modifying the exported content is prohibited, but in this example, we are doing it solely for educational purposes.
To begin, you need to download a zip archive from the settings of your Tilda website. After successfully exporting the files, you should move them to the appropriate directory for static resources.
In this example, Tilda contains a considerable number of scripts, which can complicate the integration task, especially when dealing with images. The issue arises due to the use of “magic” function calls responsible for lazy loading images. Unfortunately, this approach is not compatible with React routing, as they assume initialization on page load rather than dynamic URL switching. This problem can be resolved by making slight changes to the scripts.
It is also essential to replace all URLs for static assets (images, scripts, styles) from the Tilda domain to your own. Add links in index.html
to assets
(js, css). For instance, js/lazyload-1.3.min.js
becomes %PUBLIC_URL%/tilda/js/lazyload-1.3.min.js
.
In the code, we need to place our html
content inside a string variable:
export const TILDA_HTML = `
<div
id="allrecords"
data-tilda-export="yes"
class="t-records"
data-hook="blocks-collection-content-node"
data-tilda-project-id="4457918"
data-tilda-page-id="33161614"
data-tilda-formskey="599a4bf5b358fcc8f5ce82f8419bbb21"
data-tilda-lazy="yes"
>
...
</div>
`
And then insert it into the React component using dangerouslySetInnerHTML
:
return (
<div
dangerouslySetInnerHTML={{ __html: TILDA_HTML }}
className={"w-full bg-white rounded-2xl overflow-hidden"}
/>
);
To add event handlers, we can use the standard DOM API:
useEffect(() => {
const tildaButton = document.getElementById("tilda-button");
const onClick = () => {
alert("onClick tilda button");
};
tildaButton.addEventListener("click", onClick);
return () => {
tildaButton.removeEventListener("click", onClick);
};
}, []);
Please ensure that you add appropriate ids or classes to the elements you want to interact with.
Server-Side Rendering works well here. We render the html
content within our React component, which allows web crawlers to easily read and index the content on the page.
Every time design changes are made in the Tilda editor, manual intervention is required to go through the entire integration process. You need to download files, place them in the necessary directories, and update all the links. This complicates the automation of the process, which can be inconvenient with frequent changes.
Pros:
Cons:
Despite its simplicity, this approach is useful when you have limited time and need to quickly transfer a landing page from a builder to a React application. It allows for fast results, especially for urgent projects.
However, in the long run, this approach may become challenging to maintain and scale. After transferring the landing page, any subsequent changes to its design or content may require reintegration, reducing the efficiency and flexibility of development.
If we want to use React components in a familiar format, provide designers with the ability to work with them in a visual editor, and allow content managers to edit data separately from the design, we can consider website builders. There are many interesting solutions for designers and content managers, such as
Working with data and design is beyond the scope of this article. Let’s jump right into the code:
const [content, setContent] = useState(null);
useEffect(() => {
builder
.get(MODEL_NAME, {
userAttributes: {
urlPath: "/builder",
},
})
.promise()
.then(setContent);
const clickHandler = () => {
alert("Click button!");
};
document.addEventListener("click-button", clickHandler);
return () => {
document.removeEventListener("click-button", clickHandler);
};
}, []);
return (
<div className={"w-full h-full"}>
<BuilderComponent model={MODEL_NAME} content={content} />
</div>
);
To integrate this tool, we need to render the BuilderComponent
and pass the data we obtained from builder.get
to it. That's all that is required from the developer for a successful integration.
Additionally, we register a click-button
event that will be triggered when a button is clicked. The event configuration is done in the Builder.io admin panel. This allows us to use the event for navigating to other pages or performing actions when the button is clicked.
Additionally, we have the ability to register custom components that will be available in the Builder.io admin panel. This allows us to create and use customized components that cater to the unique needs of our project:
const CustomTicker = ({ title }) => {
// ...
return (
<div>
{title}: {count}
</div>
);
};
Builder.registerComponent(CustomTicker, {
name: "Custom Ticker",
inputs: [{ name: "title", type: "text", defaultValue: "Ticks" }],
});
Once we have registered a component, it can easily be added to the page from the left menu in Builder.io. This provides a convenient way to select and place custom components on the page for further editing and display in the React application.
When working with custom components, we have access to all the functionality of our application. For example, we can interact with the Context API to manage state and pass data between components.
Additionally, we can easily define our own handlers directly in the visual editor. This allows us to connect Headless CMS and use data in our scripts, providing greater flexibility and possibilities when developing and configuring components.
SSR works excellently if we fetch data on the server. As a result, we create HTML that is almost indistinguishable from the “native” codebase. Developers can use the
We obtain the content at the request stage, ensuring that the information is always up-to-date. There is no need to redeploy if we want to make changes to a page created with Builder.io. This provides a quick and convenient way to update content without the need for a full application restart.
Pros:
Cons:
Overall, this tool allows delegating some of the developer’s work to other team members. The main advantage lies in the clear division of responsibilities among team members and reducing interdependence.
When summarizing the subjective conclusions regarding the task of “creating pages,” it’s important to base them on personal experiences with the integration. Understanding that these tools are initially intended for different purposes, there are still some overlaps in terms of their capabilities within a React application:
Undoubtedly, the choice of the tool depends on the specific task at hand. If your goal is to deliver content, a Headless CMS would be suitable. Using an SDK might be a viable solution if you need to add specific functionality, such as forms. The purpose of this article was to inspire experimentation with various approaches and encourage the pursuit of solving business challenges using the most suitable tools.
Thank you! Best of luck in your experiments and development as well! If you have any more questions or need further assistance in the future, feel free to reach out. Happy experimenting!