Creating React “Widgets” that can be embedded on any website, by anyone

Beigisaba
5 min readDec 19, 2021

Why would I do this?
One example is for versatility in a widget you’re making. Either for a client or for the world. A widget should be embeddable in as many places as possible, regardlesss of the software. Whether that website is made using WebFlow, WordPress, Shopify, Drupal, doesn’t matter. Additionally, its common for a widget to exist multiple times on the same page. Let’s imagine a widget where we display the last 5 posts of a given subreddit. I should be able to embed that widget multiple times, for multiple subreddits, on the same page.

Keep in mind, we aren’t building this widget for React developers. If that were the case, we’d just build a React Component and publish it on npm. Instead, we’re building a widget that can be used by anyone, even a non-coder, outside of React.

We’ll go over exactly how to do this. We’ll start off by teaching you how to initialize multiple versions of your React App on the same page. Then, we’ll learn how to pass data down the DOM, into our React App. This will allow us to present each of those widgets in different ways, by setting some attributes. Attributes which your customers can easily configure, without knowing how to code.

To get started, let’s initialize a typical react app, using create-react-app.

npx create-react-app reddit-widget

ReactDOM’s Render Function
When you first initialize a React App using create-react-app, you’ll notice React attaches itself to a single element.

ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

ReactDOM‘s render function primarily takes two arguments. The first is the React Component you’ll be injecting into the DOM. The second is the actual DOM element you’ll be injecting the React Component into.

In the case above, we’re injecting our component (wrapped in React’s Strict Mode), into the #root div container in the DOM. Which you can find by navigating to public/index.html.

<div id="root"></div>

Multiple Instanes of React
Now, what happens if we want multiple instances of this React App? We know how ReactDOM’s render function works. Instead of injecting our app into a single div in the DOM, let’s inject it into multiple.

First, we’ll update index.js to iterate over multiple divs. To do this, we’ll use document.querySelectorAll and search for all divs with a reddit_widget class specified. Then, we’ll inject our React App into each of them.

const WidgetDivs = document.querySelectorAll('.reddit_widget')

// Inject our React App into each
WidgetDivs.forEach(Div => {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
Div
);
}

At this point, our React App will be blank. That’s because we don’t have any divs with the reddit_widget class yet. Let’s update our public/index.html file.

<div class="reddit_widget"></div>
<div class="reddit_widget"></div>

Great, now we have multiple versions of our React App running at the same time!

Passing Data Attributes
So we have our React App rendering multiple times in a page. This within itself isn’t useful. We want each instance of our app to contain different data or functionality.

There are tons of ways to pass data to and from a React App. In this article, we’ll cover using data attributes.
Reading DOM attributes in a React component
In React, we use Props to attach useful data to our components. In HTML, we have data attributes. Which, together with a bit of JavaScript, can be just as powerful.

First, let’s attach some data attributes to our DOM elements in public/index.html.

<div class="reddit_widget" data-subreddit="javascript"></div>
<div class="reddit_widget" data-subreddit="reactjs"></div>

Now, let’s read those data attributes in our React App. There are a number of ways we can do this.

  1. We can use Div.getAttribute(“data-subreddit”) to get our attribute from each DOM element. We can pass this a subreddit prop to our React component.

2.Similar to option 1, but using the dataset property (IE: Div.dataset.subreddit).

  1. We can pass the entire DOM element as a prop, to our React component. Allowing us to access the entire DOM element for each App. From there, we can do anything with the dom element. Including getting the attributes. For more information, check out using data attributes.

For this article, We’ll go with option 3.

// index.js 

WidgetDivs.forEach(Div => {
ReactDOM.render(
<React.StrictMode>
<App domElement={Div} />
</React.StrictMode>,
Div
);
})
// src/App.js 

function App({ domElement }) {
const subreddit = domElement.getAttribute("data-subreddit")

return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
My favorite subreddit is /r/{subreddit}
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

Great! Now we are successfully passing data from the DOM to our React App. This opens the door to tons of possibilities. We can create entirely different versions of our app, based on the attributes passed from the DOM

Example of a “real world” reddit widget
For the sake of this article, I’ll assume you’re already familiar with a few basic React concepts. IE: Data Fetching as well as Components and Props. So I won’t dive into the changes made to pull data from Reddit’s API & display the lists. If you’d like a separate article on this, please comment below. However, I feel this is already covered extensively.

To make this widget even more useful and “complete”, we’ll fetch some data from Reddit’s API. We want to include some of the latest posts, along with links to them. We also want to include a link to the subreddit itself. Finally, it’s common practice for widgets to include a “powered by” notice. Especially in a “freemium” pricing model. This allows other people to discover your widget and also become customers. Maybe even paying customers.

Here’s an example of what that looks like.

import React, { useEffect, useState } from 'react';
import './App.css';

// Render each post
function renderPost(post){
const { data: { title, url, author, id } } = post
const authorUrl = `https://www.reddit.com/u/${author}`

return (
<div className="reddit_widget__post" key={id}>
<div className="reddit_widget__posted_by">
posted by <a href={authorUrl} className="reddit_widget__posted_by" target="_blank" rel="noopener noreferrer">u/{author}</a>
</div>
<a href={url} className="reddit_widget__title" target="_blank" rel="noopener noreferrer">{title}</a>
</div>
)
}

// Filter, since reddit always returns stickied posts up top
function nonStickiedOnly(post){
return !post.data.stickied
}

function App({ domElement }) {
const subreddit = domElement.getAttribute("data-subreddit")
const [loading, setLoading] = useState();
const [error, setError] = useState('');
const [data, setData] = useState([]);

useEffect(() => {
// Fetch data from reddit
setLoading(true)
fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then((response) => response.json())
.then((data) => {
setLoading(false);
setData(data.data.children.slice(0, 10));
})
.catch((e) => {
console.log(e)
setLoading(false);
setError('error fetching from reddit');
});
}, [ subreddit ])

return (
<div className="reddit_widget__app">
<h1 className="reddit_widget__header">
Latest posts in <a href={`https://reddit.com/r/${subreddit}`} rel="noopener noreferrer">/r/{subreddit}</a>
</h1>
<div className="reddit_widget__inner">
{loading && "Loading..."}
{error && error}
{!!data.length && data.filter(nonStickiedOnly).map(renderPost)}
</div>
<p className="reddit_widget__powered_by">
This widget is powered by{" "}
<a
href="https://javascriptpros.com"
rel="noopener noreferrer"
target="_blank"
>
JavaScriptPros.com
</a>
</p>
</div>
);
}

export default App;

Building our widget
We initialized our app using create-react-app. For the sake of getting our entire bundle into a single JS & CSS file, we’ll build using parcel. Instead of completely replacing our build script, we’ll add a new one called build:widget. In this article, we won’t dive too deep into how parcel works, but feel free to check it out.

First, add parcel as a dependency

yarn add --dev parcel-bundler

Update package.json with a new build script. This tells parcel to build our JS (which will also build our css) into our docs directory. Source maps won’t be needed, to keep our build small. We chose the docs directory, so that we can publish our widget using GitHub pages, but any directory works.

"build:widget": "parcel build src/index.js --no-source-maps -d docs",

You may also want to ignore the cache directory parcel uses in .gitignore

# .gitignore

# parcel
.cache

The full code, including styling, can be seen here. You can also demo the widget itself here

--

--