Put simply, building and maintaining a completely custom notification system in-house is a pain. It requires a lot of human effort initially and will undoubtedly need to scale at some point. In addition, maintaining a system like this takes away development time from core tasks and business needs.
We adapted our offering to make sure teams don’t need to build an in-house solution for a notification systems problem. 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.
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.
We decided to make custom Courier components in React that take inspiration from Web Components to solve this. 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 allows for a custom solution that can be implemented anywhere, with any specific user interface, and with any front-end library or framework. In addition, 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. First, 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.
<body>
<section>
<h1>Hello World</h1>
<courier-toast></courier-toast>
<courier-inbox></courier-inbox>
</section>
<script type="text/javascript">
window.courierConfig = {
clientKey: "{{CLIENT_KEY}}",
userId: "{{USER_ID}}"
};
</script>
<script src="https://courier-components-xvdza5.s3.amazonaws.com/latest.js"></script>
</body>
Additional configuration options let you defer the initialization of Courier components and 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!");
};
</script>
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.
<script>
window.courierAsyncInit = () => {
window.courier.on("toast/init", () => {
window.courier.toast({
title: "Hello",
body: "World",
});
};
};
</script>
You can configure the components in two ways:
with inline HTML attributes
//inline
<courier-toast auto-close="false"></courier-toast>
with window.courierConfig
window.courierConfig = {
components: {
toast: {
autoClose: false,
}
}
};
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 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.
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 the component hierarchy. So 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.
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.
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. So 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.
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 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’re not here to add code bloat. Instead, 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 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 = {
...componentConfigs?.toast,
...getAttrsAsJson(toastElement)
};
<CourierSdk
activeComponents={{
toast: Boolean(toastElement)
}}
{toastElement &&
ReactDOM.createPortal(
<Suspense fallback={<div />}>
<Toast config={toastConfig} />
</Suspense>,
toastElement
)}
</CourierSdk>;
When a component does need to render, it can do so asynchronously. This implementation method also allows us to scale 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 can 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.
Currently, this version of our component implantation is in beta.
Courier enables developers to deliver the right message to the right user at the right time. To learn more about Courier’s full offering and see how it can integrate into your stack, check out our docs and API.
Author: Riley Napier