Building Open Source React Components for Real-Time Notifications

Emil Pearce - Aug 14 - - Dev Community

TL;DR

Components are the new API. If you’re a modern stack web developer—meaning you’re working in the JavaScript ecosystem, and either use components, or are creating your own. This is about the latter—how we re-imagined our own <Inbox /> component, and built an all-new set of components. Our new set now is a smaller bundle size, has unparalleled customization and composability, and comprehensive localization support—all designed with developers in mind. Introducing two new SDKs, @novu/js and @novu/react.


Table of Contents


Introduction and overview

The component is known by many names: Notification Popover, Notification Feed, Notification Center, Activity Feed, Updates Feed, Alert Box, Event Feed, and more.

Its name and function vary depending on the specific use case of the app. At its core, the Inbox component serves as a crucial communication channel between the app and its users.


To make sure we are on the same page, here are few examples:

Linear Inbox

It’s where you receive an Inbox notification for key events on your subscribed issues. You're automatically subscribed to issues when you create them, are assigned them, or are mentioned in an issue description or comment.

Linear Inbox


Notion Inbox & Page Notifications

The inbox and notifications in Notion help you stay on top of work that needs your attention, and changes made to the pages and projects you care about. The more people you collaborate with in your workspace, the more helpful these features become.

Notion Inbox


Slack Activity tab

Use the Activity tab on desktop or mobile to quickly review all of your notifications.
Think of it as a place to focus your attention or to get caught up after some time away from Slack. Click or tap the Activity icon on desktop or mobile to open a chronological feed of your notifications. Select a filter to switch between the different types of notifications, like mentions, replies to threads, or reactions to your messages.

Slack Activity tab


As you can already understand, the scope of an Inbox component could be very narrow or extremely robust, all comes to the app’s use case.

Building real-time event-based updates, messages, and notifications for your app can be highly challenging, especially when factoring in ongoing maintenance and future feature enhancements.

To simplify this process, we’ve developed an open-source, ready-to-use React component that provides an out-of-the-box solution for adding in-app notification capabilities, giving you a head start on your development.


Ready-To-Use Inbox

Novu Inbox Component

Here is how the code looks like

import { Inbox } from '@novu/react';

export const novuConfig = {
  applicationIdentifier: 'YOUR_APPLICATION_IDENTIFIER',
  subscriberId: 'YOUR_INTERNAL_SUBSCRIBER_ID',
};

export function Inbox() {
  return (
      <Inbox {...novuConfig} />
  );
}
Enter fullscreen mode Exit fullscreen mode

In this post, we share the “behind-the-scenes” process, providing an in-depth look at WHY and HOW we developed our new components, given that we already had functional ones. I hope you’re able to learn how you can build your own components.

If you prefer to dive right into download and test, jump to this section.


Development rationale

Meme

We broke the 1st rule of Programming, “If it works.... don't touch it!”

In May 2022, Novu released the first version of @novu/notification-center; our main repository had around 2K GitHub stars and barely a team of six people; it was a huge milestone for us!

Notification Center

Since then, we've launched numerous features designed to simplify developers' work.

As we build new technology and gain broader exposure, we accumulated valuable insights, feedback, and user requests. These drive us toward innovation, often resulting in the accumulation of technical debt and necessary refactoring.


Breaking down v1 of Novu’s Ul Inbox component collection

React component

The first comprehensive In-App component (@novu/notification-center) was a React library because that is what we knew best.

import {
  NovuProvider,
  PopoverNotificationCenter,
  NotificationBell,
  IMessage,
} from "@novu/notification-center";

function Novu() {
  return (
    <NovuProvider
      subscriberId={"SUBSCRIBER_ID"}
      applicationIdentifier={"APPLICATION_IDENTIFIER"}
    >
      <PopoverNotificationCenter colorScheme="dark">
        {({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
      </PopoverNotificationCenter>
    </NovuProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

The main Notification Center React component had 4 pieces:

  • NovuProvider - This is a wrapper component that provides the necessary context for Novu's notification center to function. It should be placed at the top level of your application hierarchy.
  • PopoverNotificationCenter - This is a pre-built component that displays notifications in a popover format.
  • NotificationBell - This component renders the notification bell icon. It's typically used within the PopoverNotificationCenter and can display the number of unseen notifications.
  • IMessage - This is an interface type representing a notification message. It contains information about the notification, including its content, status, and any associated actions.

Web component

Novu wanted to cater to all its community members and users. So… The next package wasn’t actually a package; it was another component within @novu/notification-center. We called it “Web Component*”.*

import { NotificationCenterWebComponent } from "@novu/notification-center";
Enter fullscreen mode Exit fullscreen mode

The Notification Center Web Component was a custom element that can be used in any web application. It was built by wrapping the React components and consists from its core the three pieces:  NovuProviderPopoverNotificationCenter, and  NotificationBell combined together into one. Our first bundle!

Vue component

The @novu/notification-center-vue package provided a Vue component wrapper over the Notification Center Web Component that you can use to integrate the notification center into your Vue application. The main issue is that it’s using React along with ALL THE DEPENDENCIES under the hood.

Angular component

The @novu/notification-center-angular package provided an
Angular component wrapper over the Notification Center Web Component
that you can use to integrate the notification center into your Angular
application.

iframe embed

If you were using an unsupported client framework, you could use our embedded script. This generated the notification center inside an iframe.

With every new wrapper, time, frequent questions, requests, and issues started to pile up, like:

  • Very big bundle size
  • Limitations and general friction in customization and styling.
  • Many warnings during webpack compile process
  • “Delete All Notifications” option
  • Marking a message as "seen" briefly reverts to "unread" during data updates

And more.

We made bug fixes and feature enhancements and even released the @novu/headless, offering a package with just the essential API methods so that users could easily incorporate Novu’s notification system into any framework or vanilla JavaScript project without being constrained by our default UI or dependencies.

With the team expansion, new people started to bring new questions and perspectives to the table.


Development process for the Inbox Component

With the launch of our code (and developer) first code-based @novu/framework workflow approach, we set out to re-evaluate other areas we could make developer’s lives easier. We already believed that components were the answer to drastically reduce the time and complexity required to provide end users a powerful in-app experience. Brainstorming ways we could improve the experience became the next step.

Our straightforward guiding principles

  • How can we help make it easier for our users to implement an in-app notifications component?
  • How might we ensure that our users have enough flexibility to deliver the experience they need to provide to their end users?
  • What can we do to make the developer a hero inside their business?

Brainstorming and feature selection

We started with a brainstorming session to list down the features we wanted in the new Inbox component. After the session, we selected the features to tackle in the first release.

Miro brainstorming

Feature selection

  • First release: Features for the first release were chosen based on team discussions and planning sessions.
  • Future versions: Features for future versions are discussed in ongoing planning meetings and are captured on the Linear board and the roadmap.

Design phase

Nikolay, our Product Designer took the requirements and created initial inbox component designs.

whitelabel 1

Bell Icons

Preferances

Whitelabel 2

Whitelabel 3

Planning and breaking down designs

The team (in this case, Pawel and Biswa) started breaking down the design into smaller pieces to create tickets on Linear.

They conducted several POCs and planning sessions to decide on the packages to create and the framework to use for the UI.

flow

Initial planning

  • Packages to create

    • @novu/js: The foundational package that will be used by other Novu packages such as @novu/react and @novu/vue (in the future).

      This package includes everything (headless and UI components).

    • @novu/react: Chosen due to React's popularity and significant customer base.


Package development

@novu/js

  • Headless: API client and Novu classes for custom UIs, replacing the old @novu/headless due to its complexity and unclear documentation.
  • UI Components: Exporting UI components designed in Figma:
    • Inbox: Default all-in-one component.
    • Bell: Includes unread count and customizable bell icon.
    • Preferences: Full-page list for user settings.
    • Notifications: Full-page notifications similar to Twitter's notification feed.

@novu/react

  • Consumes UI from @novu/js and renders it in React. Each component can be fully replaced by the user’s custom components, offering flexibility and customization.

UI framework research

The team then researched various UI frameworks/libraries, including

  • SolidJS
  • React
  • Vue
  • Lit
  • Svelte

They created some POCs with these frameworks and decided on SolidJS due to its lightweight nature, superior performance compared to React, and similar syntax to React, which facilitated ease of adoption.


CSS solutions research

The team researched CSS solutions and customization levels while considering naming conventions for the headless part. Biswa was responsible for researching CSS solutions, and Pawel worked on headless naming conventions.

CSS solutions candidates

  • vanilla-extract
  • Panda CSS (Not chosen due to suboptimal experience)
  • Stitches
  • Styled-jsx
  • Linaria
  • Tailwind
  • Plain CSS

We chose Tailwind because of compilation to pure CSS classes and better DX.

We aim to provide developers with multiple ways to customize the Novu Inbox component styling.

The available customization options include

  • Style objects
  • Classnames
  • CSS variables
  • Target classnames

API endpoints

We introduced a new /inbox endpoint path instead of using the existing notification center API due to various changes in the Notification DSL and the need for more straightforward, leaner endpoints.

Our docs won’t publicly expose these endpoints. (We might expose the new endpoints in the future if our users share their needs and use cases). Instead, we encourage our users to use our Component ****SDK and higher-level library UI packages.

A ‹Component/› is worth a thousand APIs

With amazing notification UI components as our primary goal, we realized that Novu wouldn't be a traditional API company. We focused on building a fantastic frontend component SDK, rather than a traditional backend REST SDK.

Components are a higher level of abstraction than REST SDKs. They provide UIs to end-users instead of just functions to developers, so they inherently offer more value.


Localization

Our team mate George worked on Localization. Novu needs to offer a way to override the default copy and localize the content of the <Inbox/> component.

The most intuitive way to do this is via a localization prop.

import { Inbox } from '@novu/react';

function Novu() {
  return (
    <Inbox
      options={{
        subscriberId: 'SUBSCRIBER_ID',
        applicationIdentifier: 'APPLICATION_IDENTIFIER',
      }}
      localization={{
        'inbox.status.archived': 'Archived',
        'inbox.status.unread': 'Unread',
        'inbox.status.options.archived': 'Archived',
        'inbox.status.options.unread': 'Unread',
        'inbox.status.options.unreadRead': 'Unread/Read',
        'inbox.status.unreadRead': 'Unread/Read',
        'inbox.title': 'Inbox',
        'notifications.emptyNotice': 'No notifications',
        locale: 'en-US',
      }}
    />
  );
}

Enter fullscreen mode Exit fullscreen mode

The localization object should remain flat for simplicity and maintainability.

flat

<Inbox localization = {{
    'notification.primaryButtonText': 'Accept',
}}
Enter fullscreen mode Exit fullscreen mode

We might provide localization templates for all languages via a new localization package, or provide a locale prop and localize automatically (could auto-detect too).


Notification schema improvement

We planned to improve the Notification Schema for a better Developer Experience. Biswa created a DX guide for the updated schema and coordinated with Richard from the framework team to ensure alignment.

// ---- EXISTING ENUMS ---- 
enum ChannelTypeEnum {
  IN_APP = 'in_app',
  EMAIL = 'email',
  SMS = 'sms',
  CHAT = 'chat',
  PUSH = 'push',
}
// existing enum
enum ButtonTypeEnum {
  PRIMARY = 'primary',
  SECONDARY = 'secondary',
}
// ------------------------------

// ======== NEW TYPES =======
type Action = {
  label: string;  // Human-readable label for the action.

  isCompleted: boolean; // Indicates whether the action is completed (done/pending)
};

type Subscriber = {
    id: string;
    firstName?: string;
    lastName?: string;
    avatar?: string;
    subscriberId: string;
};

type Redirect = {
     url: string;

     // maybe in V2
     // REF: https://developer.mozilla.org/en-US/docs/Web/API/Window/open#target
     target: '_self' | '_blank' | '_parent' | '_top' | '_unfencedTop';
}

type InboxNotification = {
  id: string;  // Unique identifier for the notification.

  subject?: string;  // Subject/Title of the notification, optional for backward compatibility

  body: string;  // Content/body of the notification.

  to: Subscriber;  // Recipients of the notification.

  read?: boolean;

  archived?: boolean;

  createdAt: string;  // Timestamp indicating when the notification was created.

  readAt?: string;  // Timestamp indicating when the notification was read, optional.

  archivedAt?: string;  // Timestamp indicating when the notification was archived, optional.

  avatar?: string; // The icon URL

  primaryAction?: Action;

  secondaryAction?: Action;

  channelType: ChannelTypeEnum;  // Type of channel through which the notification is sent.

  tags?: string[]; // Array of tags that were applied on the workflow

  redirect?: Redirect;  // URL for performing the redirect on notification click, optional.
}
Enter fullscreen mode Exit fullscreen mode

Notification schema

Defines properties of notification (e.g., subject, content, avatar, read status, archived status) and how it is represented in code and the database. This schema change was also a factor in opting for new endpoints.


Comparison

@novu/notification-center vs. @novu/react

This comparison highlights the differences between the old @novu/notification-center library and the new @novu/react library, emphasizing the improvements and benefits introduced in the new version.

Feature @novu/notification-center @novu/react
Component structure Multiple components (NovuProvider, PopoverNotificationCenter, NotificationBell) Single All-in-One, and separate dedicated components.
Installation npm install @novu/notification-center npm install @novu/react
Setup complexity Moderate complexity with multiple components Simplified setup with a single component
Customization Limited options Extensive customization options (backend URL, socket URL, state management, localization)
Security Basic security, requires manual setup for HMAC encryption Enhanced security with built-in HMAC encryption support
State management No direct state management for popover Direct state management via open prop
Localization Not supported Supported via localization prop
Real-time updates Uses useSocket hook for real-time updates Establishes the single active WebSocket connection that is shared between multiple tabs.
Notification actions Separate handlers for onNotificationClick and onActionClick Streamlined handling within the Inbox component
API design More complex and less intuitive Simplified and more intuitive
Open source Yes! Yes, and now more extensible and community-building.

Wrapping up

Because our goal is to help Novu users implement an in-app inbox, ensure that they have enough flexibility, and make them heroes at their company, we are doubling down on providing the new gold standard in developer tools: UI notification components, starting from our new Inbox component.

And even if frontend developers don't like our components for any reason, they can still build their own UIs without involving a backend developer and a backend SDK.

Once again. we invite you to explore the new @novu/js and @novu/react packages. These tools are designed to make your development process smoother and more efficient.


Novu's JavaScript SDK

Installation

Install @novu/js npm package in your app

npm install @novu/js
Enter fullscreen mode Exit fullscreen mode

Getting started

Add the code below to your application

import { Novu } from '@novu/js';

const novu = new Novu({
  applicationIdentifier: 'YOUR_NOVU_APPLICATION_IDENTIFIER',
  subscriberId: 'YOUR_INTERNAL_SUBSCRIBER_ID',
});

const { data: notifications, error } = await novu.notifications.list();
Enter fullscreen mode Exit fullscreen mode

Novu's React SDK

Installation

Install @novu/react npm package in your app

npm install @novu/react
Enter fullscreen mode Exit fullscreen mode

Getting started

Add the below code in the app.tsx file

import { Inbox } from '@novu/react';

function Novu() {
  return (
    <Inbox
      options={{
        subscriberId: 'SUBSCRIBER_ID',
        applicationIdentifier: 'APPLICATION_IDENTIFIER',
      }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

Learn more in our changelog and docs


Liked what you read? Hit follow for more updates and drop a comment below. I’d ❤️ to hear your 💭

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .