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

courier and expo push notifications
GuideEngineering

Expo Push Notifications: The Complete Implementation Guide (SDK 52+)

Expo push notifications are alerts sent from a server to a user's phone, even when the app isn't open. To set them up, install the expo-notifications library, ask the user for permission, and get a unique push token for their device. Your server sends a message to Expo's push service with that token, and Expo delivers it through Apple or Google. Push notifications only work on real phones, not simulators. Local notifications are different — they're scheduled by the app itself for things like reminders. You can also route Expo push through services like Courier to add email, SMS, and Slack fallbacks.

By Kyle Seyler

February 24, 2026

email infrastructure providers
AIGuideEngineering

Best Email API Providers for Developers in 2026: SendGrid vs Postmark vs Mailgun vs SES vs Resend

Your email provider sticks with you longer than most technical decisions. Courier handles notification infrastructure for thousands of teams, so we went deep on the six email providers that show up most: SendGrid, Postmark, Mailgun, Amazon SES, Resend, and SMTP. This guide covers real API primitives, actual code from each provider's docs, Courier integration examples with provider overrides, and an honest read on where each developer experience holds up and where it breaks down. We also asked Claude to review every API and tell us which one it would wire up first. The answer surprised us.

By Kyle Seyler

February 23, 2026

Courier MCP is open source
AIEngineering

The Courier MCP Server Is Open Source. Here's How It Actually Works.

Courier's MCP server is open source at github.com/trycourier/courier-mcp. It connects AI coding tools like Cursor and Claude Code to your Courier account so they can send messages, manage users, and install SDKs without hallucinating API details. This post walks through the actual codebase: how 16 tool classes are registered (and how a config allowlist gates most of them), why we pull installation guides from GitHub at runtime instead of bundling them, how the DocsTools class generates live JWTs alongside setup instructions, and what the SdkContextTools class does in the repo to prevent v7/v8 SDK conflicts (even though it isn't wired into the server yet).

By Mike Miller

February 06, 2026

Multichannel Notifications Platform for SaaS

Products

Platform

Integrations

Customers

Blog

API Status

Subprocessors


© 2026 Courier. All rights reserved.