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:
- Changing page content in React without involving a developer;
- Extending the functionality of the React application using third-party services;
- Creating pages with minimal development costs.
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.
Conditions
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:
- Ease of adoption: The tool should be straightforward and easy to learn for all parties involved — how to implement and maintain it, manage its visual aspects, and handle content.
- Flexibility: While it may not be possible to create a one-size-fits-all solution for every case, the tool should fulfill its purpose and ideally offer possibilities for extending its functionality.
- Level of interaction: We want to provide users with rich interactive pages. Is it possible to dynamically display different elements depending on the state of the React application, such as authentication, and invoke functions from the main application?
- Ease of integration: How simple and fast is the process of initial integration and subsequent deployments? Can this process be automated?
- Clarity of integration: Some tools are specifically designed for integration, while others are not. How much effort will be required to display and interact with integrated content?
In the examples, we will address the following questions:
- How to display the results of a tool’s work within our React application in a dedicated container?
- How to subscribe to events from React?
- What possibilities exist for Server Side Rendering?
It is worth noting that I will provide examples of solutions using specific products, such as Tilda, Hubspot, Typeform, Contentful,
Task: Changing content
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.
Integration
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.
Server Side Rendering
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.
Deploy
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
Summary
Pros:
- Content managers can update page content without involving developers, significantly saving the team’s time.
- Changes happen in real-time, which is critical when the application deployment takes considerable time, allowing instant content updates without reloading the entire application.
- Ability to address tasks related to meta tags for the page, such as title, description, keywords, and other metadata.
Cons:
- When using a large number of fields in Contentful, it’s important to manage them and use meaningful names, especially if the page contains multiple headings and various data.
- Despite offloading content management to an admin interface, manual coding is still required for the layout. If the design changes, modifications will be needed in the application’s code. Additionally, if new variables or content blocks appear, they must be added to the codebase.
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.
Task: Expansion of functionality
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.
Integration
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.
Server Side Rendering
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.
Deploy
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
.
Summary
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:
- Does not work with SSR (Server Side Rendering).
- Not all solutions provide SDKs, which may require writing code wrappers. This can add complexity and increase development time.
Task: Creating Pages
Method 1: Using Iframe
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.
Integration
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:
- It’s essential to filter events by checking the
origin
field, using the conditionevent.origin.includes("hubspot")
. This is crucial because themessage
event can come from variousiframe
andworkers
, and we need to process only those events originating from Hubspot. - When we receive an action with the type
click-button
generated on the Hubspot side, we handle the click in React. This way, we can react to actions happening inside theiframe
and synchronize them with the React application. - We send the
set-config
action only after theiframe
has loaded (action typeload
- 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:
- The
iframe
is loaded, and theDOMContentLoaded
event is triggered. - In the parent application (React), we subscribe to the
message
event. - We send an
action
with typeload
to notify the React application that this script is ready to receiveconfig
. Here, we can also pass metadata about the page to add them to the React application's<head>
section for SEO purposes. - Upon receiving the
set-config
action, we iterate through all the links and buttons specified in theconfig
and addclick
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.
Server Side Rendering
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.
Deploy
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.
Summary
Pros:
- Ability to implement functionality of any complexity using both our React application and a third-party service.
- No need to involve developers to change the content of the page.
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.
Method 2: Export HTML/CSS/JS
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.
Integration
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"}
/>
);
Interaction
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
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.
Deploy
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.
Summary
Pros:
- Quick integration of ready-made landing pages into your React application. Estimated transfer time is about 2 hours of work.
Cons:
- Lack of automation for the integration process, requires manual intervention.
- Unclear behavior of scripts, which can lead to various issues.
- Official restrictions on modifying exported content from such resources.
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.
Method 3: React App Builder
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
Integration
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.
Interaction
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.
Server Side Rendering
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
Deploy
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.
Summary
Pros:
- Easy integration for React developers. We simply need to add the BuilderComponent to the page and register our custom components.
- Ability to import designs from Figma. However, there might be some nuances and adaptation of the designs for export is required.
- Increased development speed. By training designers and content managers on this tool, pages can be created without the need for developer intervention.
Cons:
- The visual editor is not as powerful as other design tools for designers. Understanding CSS is required, as in some cases, CSS code needs to be written directly.
- Entry barrier. The presence of multiple abstractions may require a few days of learning the concept and understanding the authors’ intentions.
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.
Comparison
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:
- Event Handling: Iframe(Hubspot), Export html(Tilda), Constructor(Builder.io)
- Visual Editor: Iframe(Hubspot), Export html(Tilda), Constructor(Builder.io)
- Routing: Iframe(Hubspot), Export html(Tilda), Constructor(Builder.io)
- Automation of Deployments: Iframe(Hubspot), Constructor(Builder.io)
- Separation of Data and Design: Iframe(Hubspot), Constructor(Builder.io)
- Content Indexing (SSR): Export HTML(Tilda), Constructor(Builder.io)
- SDK: Constructor(Builder.io)
- Integration Documentation: Constructor(Builder.io)
Conclusion
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!