Headless mode

Learn how to build custom Inbox UI for your application using Novu custom hooks

Build a fully custom notification inbox with Novu's headless React hooks without being constrained by the default <Inbox /> UI or dependencies.

The @novu/react package provides a set of hooks, such as useNotifications and useCounts. You handle the layout, styling, and interactions, while Novu provides the notification state, real-time updates, and actions.

Not using React? You can access the same data manually using the JavaScript SDK.

Install the React SDK package

Run the following command in your terminal:

npm i @novu/react

Add the NovuProvider

Wrap your application or the components that need access to notifications with the NovuProvider. This component initializes the Novu client and provides it to all child hooks via context.

import { NovuProvider } from '@novu/react';
 
function App() {
  return (
    <NovuProvider
      subscriber="SUBSCRIBER_ID"
      applicationIdentifier="APPLICATION_IDENTIFIER"
    >
      {/* Your app components */}
    </NovuProvider>
  );
}
For more NovuProvider options, such as HMAC encryption, see the Novu provider documentation.

Fetch and display notifications

Use the useNotifications hook to fetch and display a list of notifications. The hook manages loading states, pagination (hasMore, fetchMore), and real-time updates for you.

import { useNotifications } from "@novu/react";
 
function NotificationsList() {
  const { notifications, isLoading, error } = useNotifications();
 
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <div className="space-y-4">
      {notifications?.map((notification) => (
        <div key={notification.id} className="p-4 border rounded-lg">
          <h3 className="font-medium">{notification.subject}</h3>
          <p>{notification.body}</p>
        </div>
      ))}
    </div>
  );
}

Show notification counts

A common use case is showing a badge with the unread count on a bell icon. The useCounts hook is designed for this. It fetches unread, unseen, or total counts and updates them in real-time.

import { useCounts } from "@novu/react";
 
function BellButton() {
  const { counts } = useCounts({ 
    filters: [
      { read: false }, // Unread notifications
    ] 
  });
  
  const unreadCount = counts?.[0]?.count ?? 0;
 
  return (
    <button>
      <BellIcon />
      {unreadCount > 0 && (
        <span className="badge">{unreadCount}</span>
      )}
    </button>
  );
}

Adding notification actions

To perform actions on notifications such as marking as read, unread, or archiving, you can use the useNovu hook. This hook gives you direct access to the Novu client instance to call its API methods.

The hooks useNotifications and useCounts automatically refetch and update your UI when these actions are performed.

import type { Notification as INotification } from "@novu/react";
import { useNovu } from "@novu/react";
 
function NotificationItem({ notification }: { notification: INotification }) {
  const novu = useNovu();
 
  const markAsRead = async () => {
    try {
      await novu.notifications.readAll({ notificationId: notification.id });
    } catch (error) {
      console.error("Failed to mark as read:", error);
    }
  };
 
  const archive = async () => {
    try {
      await novu.notifications.archiveAll({ notificationId: notification.id });
    } catch (error) {
      console.error("Failed to archive:", error);
    }
  };
 
  return (
    <div className="p-4 border rounded-lg">
      <h3 className="font-medium">{notification.subject}</h3>
      <p>{notification.body}</p>
      <div className="flex gap-2 mt-2">
        <button
          onClick={markAsRead}
          className="px-2 py-1 text-sm bg-blue-50 text-blue-600 rounded"
          disabled={notification.isRead}
        >
          Mark as read
        </button>
        <button
          onClick={archive}
          className="px-2 py-1 text-sm bg-gray-50 text-gray-600 rounded"
          disabled={notification.isArchived}
        >
          Archive
        </button>
      </div>
    </div>
  );
}

Real-time updates

The useNotifications and useCounts hooks automatically listen for real-time events and update your component state.

If you need to listen for events manually, then you can use the novu.on() method from the useNovu hook. For example, you might want to show a toast notification when a new message arrives.

import { useNovu } from "@novu/react";
import { useEffect } from "react";
import type { Notification as INotification } from "@novu/react";
 
function NotificationListener() {
  const novu = useNovu();
 
  useEffect(() => {
    // Handler for new notifications
    const handleNewNotification = ({ result }: { result: INotification }) => {
      console.log("New notification:", result.subject);
      // You can use a toast library here
      // toast.show(result.subject);
    };
 
    // Handler for unread count changes
    const handleUnreadCountChanged = ({ result }: { result: number }) => {
      document.title = result > 0 ? `(${result}) My App` : "My App";
    };
 
    // Subscribe to events
    novu.on("notifications.notification_received", handleNewNotification);
    novu.on("notifications.unread_count_changed", handleUnreadCountChanged);
 
    // Cleanup function
    return () => {
      novu.off("notifications.notification_received", handleNewNotification);
      novu.off("notifications.unread_count_changed", handleUnreadCountChanged);
    };
  }, [novu]);
 
  return null; // This component doesn't render anything
}

On this page

Edit this page on GitHub