DEV Community

Cover image for Creating React “Widgets” that can be embedded on any website, by anyone
Gio
Gio

Posted on • Originally published at javascriptpros.com

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

A software widget is a relatively simple and easy-to-use software application or component made for one or more different software platforms.
read more

Some of examples of a widget are the related content widget by Taboola. Or the newsletter widget, by MailChimp. In this article, we'll teach you how to build a reddit widget.

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 <App /> 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

view final commit

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.

// Find all widget divs
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! This is pretty much the foundation for this article ⚡️

Multiple-Instanes-of-React

Passing Data Attributes

view final commit

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 <App/> component.
  2. Similar to option 1, but using the dataset property (IE: Div.dataset.subreddit).
  3. We can pass the entire DOM element as a prop, to our React <App /> 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>
  );
}

Reading DOM attributes in a React component

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

view final commit

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

view final commit

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

See our widget in action

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

And here's what that looks like 🧐

(note: at the time of writing, the image below is broken, here is a direct link)

--

Full Reddit Widget built with React
Full Reddit Widget built with React

Enabling non-developers to use our widget

When providing instructions to a customer on how to use the widget, we'd probably send them instructions that look something like this:

Copy these 3 lines of code and replace SUBREDDIT_HERE with the subreddit of your liking.
You can add more than one widget by duplicating only the 3rd line.

<link href="https://giologist.github.io/article-react-reddit-widget/index.css" rel="stylesheet" />
<script src="https://giologist.github.io/article-react-reddit-widget/index.js"></script>
<div class="reddit_widget" data-subreddit="SUBREDDIT_HERE"></div>

Things to consider

  • React may not always be necessary, or the best tool for a smaller widget. If keeping bundle size down is your main priority, you may want to consider building your widget simply using vanilla javascript.

  • If your widget is going to load its own css, be sure not to include style properties for common elements such as html and body. You don't want to override the styling on someone else's page. Keep your styling specific to only your widget.

Any questions? Feel free to drop a comment.

Top comments (2)

Collapse
 
ganeshmani profile image
GaneshMani

Good article. is it possible to create the embeddable code something like this?

<script>
    (function (w, d, s, o, f, js, fjs) {
        w['Simple-Widget'] = o; w[o] = w[o] || function () { (w[o].q = w[o].q || []).push(arguments) };
        js = d.createElement(s), fjs = d.getElementsByTagName(s)[0];
        js.id = o; js.src = f; js.async = 1; fjs.parentNode.insertBefore(js, fjs);
    }(window, document, 'script', 'w1', 'http://somehost/widget.js'));
    w1('init', { targetElementId: 'root' });
</script>
Collapse
 
derp_unicorns profile image
Shooting Unicorns

Thank you so much for this article Gio, you couldn't have written it at a better time!

Works like a charm 😄