Blog
ENGINEERING

How We Built React Components for Any Front End

Riley Napier

June 30, 2021

How Courier Exposes React Elements Header

Table of contents

__We built custom Courier components__

__We preserved context with React Portals__

__We resourcefully packaged a solution__

__Try it yourself__

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.

Copied!

<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, 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.

Copied!

<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.

Copied!

<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

Copied!

//inline
<courier-toast auto-close="false"></courier-toast>
  • with window.courierConfig

Copied!

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 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 withReact 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.

Copied!

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 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.

Similar resources

React Native and Courier
IntegrationsEngineering

React Native Push Notifications: FCM, Expo, and Production-Ready

React Native lets you ship to iOS and Android from one codebase, but push notifications still require platform-specific infrastructure. This guide covers implementing push with Firebase Cloud Messaging (FCM) for bare React Native and Expo Push for managed workflows. Both get basic push working, but production apps quickly hit limitations: no delivery confirmation, no fallback channels, no user preferences, and debugging is guesswork. Learn why teams add a notification orchestration layer to handle token lifecycle, multi-channel coordination, user preferences, and delivery observability. Includes code examples for authentication, in-app inbox components, preference centers, and multi-channel routing with automatic fallbacks.

By Kyle Seyler

January 07, 2026

flutter plus courier
EngineeringNotifications Landscape

Flutter Notifications: Add In-App Inbox and Push in 10 Lines of Code

Flutter 3.38 is production-ready, but most teams still waste weeks building notification infrastructure. FCM setup, token management, inbox UI, preference centers, cross-channel state sync. Skip all of it. Add a complete notification system in 10 lines: real-time inbox, push for iOS/Android, digest batching, GDPR preferences. When users open an email, inbox messages auto-mark as read. Courier handles FCM/APNs tokens, WebSocket reconnection, and compliance. Same infrastructure Twilio uses for 10M+ developers.

By Kyle Seyler

January 07, 2026

investigation workflow
CourierEngineering

How We Investigate Support Tickets at Courier

Courier's support team resolves complex issues 4X faster using parallel investigation. Here's how it works: when a ticket comes in, an AI agent starts exploring the codebase while the support engineer examines actual customer data. The agent traces code paths and searches past investigations. The human reads event logs and forms hypotheses based on real state. Running both simultaneously catches mismatches fast—the agent sees what could cause a problem, the human sees what actually happened. This post breaks down the workflow, tools, and documentation structure that makes it repeatable.

By Thomas Schiavone

December 18, 2025

Multichannel Notifications Platform for SaaS

Products

Platform

Integrations

Customers

Blog

API Status

Subprocessors


© 2026 Courier. All rights reserved.