DEV Community

Cover image for How We Built React Components for Any Front End
Riley for Courier

Posted on • Updated on • Originally published at

How We Built React Components for Any Front End

Put simply, building and maintaining a completely custom notification system in-house is a pain. It requires a lot of human effort in the beginning and will undoubtedly need to scale at some point. Maintaining a system like this takes away development time from core tasks and business needs.

To make sure teams don’t need to build an in-house solution for a notification systems problem, we adapted our offering. We created a lightweight solution using React that has a global state and runs independently in the background — so teams can render our components regardless of their tech stack.

We built custom Courier components

While React is a popular library, we recognize not everyone uses it, and it might not be as widely used in the future as competing front-end architectures emerge. This is why we wanted to find a way to create custom components that can work in any front end setup with any user interface.

To solve this we decided to make custom Courier components in React that take inspiration from Web Components. The idea behind Web Components is that they allow developers to build custom, reusable elements where the functionality lives independently from other parts of the codebase.

This modular setup is what allows for a custom solution that can be implemented anywhere, with any specific user interface, and with any front-end library or framework. Because the logic can live outside the context of your other code, our components can run independently in the background.

The initial setup is straightforward. You place two script tags in the body (the order of the tags is important). The first script tag holds a small amount of code where you identify configurations like your user with a userId and your Courier clientKey. The second script tag downloads the Courier components.

    <h1>Hello World</h1>
  <script type="text/javascript">
    window.courierConfig = {
      clientKey: "{{CLIENT_KEY}}",
      userId: "{{USER_ID}}"
  <script src=""></script>
Enter fullscreen mode Exit fullscreen mode

Additional configuration options let you defer the initialization of Courier components, as well as map the configuration for each component you load on the page. The two components you can currently load are toast and inbox.

Our SDK is exposed on window.courier and is loaded asynchronously. Calling window.courierAsyncInit will let you know Courier has successfully loaded.

<script type="text/javascript">
  window.courierAsyncInit = () => {
    console.log("Courier is Ready!");
Enter fullscreen mode Exit fullscreen mode

If you’d prefer to separate the logic for each component (the toast and inbox components), you can also choose to set window.courierAsyncInit to an array.

After initialization, window.courier is ready, and you can listen for actions inside the Courier SDK. A small amount of code lets you init the toast component.

  window.courierAsyncInit = () => {
    window.courier.on("toast/init", () => {
        title: "Hello",
        body: "World",
Enter fullscreen mode Exit fullscreen mode

You can configure the components in two ways:

with inline HTML attributes

<courier-toast auto-close="false"></courier-toast>
Enter fullscreen mode Exit fullscreen mode

with window.courierConfig

window.courierConfig = {
 components: {
  toast: {
   autoClose: false,
Enter fullscreen mode Exit fullscreen mode

If you need to use multiple configuration options with a component, window.courierConfig gives you that ability without having to add too many attributes to your HTML element.

If you do choose to use the inline configuration, you’ll need to make sure you’re always formatting in kebab case since HTML attributes are not case sensitive.

We preserved context with React Portals

It’s pretty easy to get up and running with the components. But one hurdle we needed to overcome was making sure the data you need from us is accessible to every Courier React component. And this needs to happen anywhere in your project, regardless of component hierarchy. We make use of React Context and React Portals to inject components anywhere in your DOM.

If you’re unfamiliar with React Context and React Portals, here’s a quick rundown.

React Context

Context allows you to pass props between components without explicitly having to deal with tree structure. This allows for easy access to data regardless of UI requirements. The result is global data accessible by child components that live outside the nesting levels of parent components that contain necessary data.

React Portals

The use of a portal allows you to inject a child anywhere into the DOM, retaining the context of the parent node even though it’s outside the standard nesting structure. Even though the portal can be placed randomly in the DOM tree, the portal still retains its context in the React tree. This means events like bubbling will still function normally.

Putting it all together

After the initialization of Courier, we analyze the HTML and find components to dynamically import, making sure not to download any extra components you aren't using. We identify them by HTML tags and then render them inside the context of the Courier SDK. This allows us to then render them wherever you need in the DOM with the Courier context they need.

So through a combination of React Context and React Portals, we preserve the global state our Courier components rely on. Our toast and inbox components render into a portal, and the portal allows for those components to act as children out of the hierarchy order of the parent. This allows you to render our Courier components into anything that's not in the official React DOM tree.

We resourcefully packaged a solution

We’re not here to add code bloat. We purposefully found solutions that guarantee we keep our integration as small as possible.

We currently have two components you can render, the toast message and the inbox. We're cognizant that library size matters, and while some might see a need to integrate both components, others might only want to integrate one. We also have plans to add more components in the future, so it's important to dynamically load what's needed, not everything.

By providing a small amount of code for you to implement that handles the automatic download of desired components, we make sure your project remains as small and lightweight as possible. When you load our code, we analyze your HTML to see what components you’ve identified that you need. These components are loaded dynamically and are then cached. This ensures that subsequent renders aren’t refreshing the code.

We do this with React Suspense, which does exactly what it says. It suspends the rendering of React components until a condition is met. In the example below, the portal we’ve created is waiting to see if the toast component has a configuration set up. If it does, we will load it.

import React, { lazy, Suspense } from "react";

const toastElement = document.querySelector("courier-toast") ?? undefined;
const toastConfig = {

    toast: Boolean(toastElement)
  {toastElement &&
      <Suspense fallback={<div />}>
        <Toast config={toastConfig} />
Enter fullscreen mode Exit fullscreen mode

When a component does need to render, it can do so asynchronously. This implementation method also allows us to scale in the future by adding new components that can be dynamically imported.

In addition to dynamically imported components, we also keep the bundle small by using Preact. Preact uses the same ES6 API as React, but Preact is more lightweight and able to load a faster, thinner virtual DOM. We’ve carefully built this implementation so Preact can fully replace all instances of React.

You can check out the repo here.

Try it yourself

Courier enables developers to deliver the right message to the right user at the right time. To find out more about Courier’s full offering and see how it can integrate into your stack, check out our docs and our API.

Top comments (0)