React inbox widget
Drop a notification inbox into a React app with one component. The @dudousxd/nestjs-notifications-react package ships <Inbox/>, a NotificationsProvider, and the useNotifications / useUnreadCount hooks — all consuming the read API and SSE stream you already expose.
You already have the backend inbox: the database channel persists
notifications, the REST controller exposes them, and the
SSE channel pushes live updates. @dudousxd/nestjs-notifications-react is the
frontend half — a bell, a badge, a dropdown, and hooks — so you don't hand-write fetch calls and
EventSource wiring.
pnpm add @dudousxd/nestjs-notifications-reactThe one-liner: <Inbox/>
Point it at your API and you have a working inbox — bell, unread badge, dropdown list, mark-as-read, mark-all-read, and infinite scroll:
import { Inbox } from '@dudousxd/nestjs-notifications-react';
export function AppHeader() {
return (
<header>
<Inbox
clientOptions={{ baseUrl: '/notifications' }}
sseUrl="/notifications/stream"
/>
</header>
);
}That's the whole integration. clientOptions.baseUrl is where the
REST controller is mounted; sseUrl is the
live stream endpoint. Drop sseUrl and the widget falls back to polling.
Configure once with NotificationsProvider
If several components need the inbox (a header bell, a settings page, a count somewhere else), wrap your app once so the client and stream URL are shared:
import { NotificationsProvider } from '@dudousxd/nestjs-notifications-react';
root.render(
<NotificationsProvider
clientOptions={{ baseUrl: '/notifications', credentials: 'include' }}
sseUrl="/notifications/stream"
>
<App />
</NotificationsProvider>,
);Now <Inbox/> and the hooks read their config from context — no props needed.
The hooks
When you want your own UI, the hooks give you the data and the mutations:
import { useUnreadCount } from '@dudousxd/nestjs-notifications-react';
const { count, refresh } = useUnreadCount();count updates live off the SSE stream (or polls when there's no sseUrl). This is the
live badge in one line.
import { useNotifications } from '@dudousxd/nestjs-notifications-react';
const {
notifications, // the loaded page(s), newest first
loading,
error,
hasMore,
loadMore, // fetch the next page
markAsRead, // (id) => optimistic, rolls back on error
markAllAsRead,
remove, // (id) => delete
refresh,
} = useNotifications();Mutations are optimistic — the UI updates immediately and rolls back if the request fails.
Cross-device read sync
useNotifications() also returns applyReadEvent — patch the local list when another tab or device
marks a notification read, so this view updates without a refetch. Feed it the stream's onRead:
const { applyReadEvent } = useNotifications();
useNotificationsStream({ url: '/notifications/stream', onRead: applyReadEvent });onRead fires with a ReadSyncEvent ({ notificationId, readAt }; notificationId: null = all
read). <Inbox/> and NotificationsProvider already wire this when an sseUrl is set — you only
need it when you compose the hooks yourself. The server side (binding the SSE read publisher) is in
the Real-time & in-app guide.
Customizing the row
<Inbox/> renders a sensible default row from item.data: title, body, relative time, and — when
present — a progress bar (data.progress, 0–100) for long-running notifications and a
download/action link (data.action of { label, url }, or a flat actionUrl/downloadUrl).
The same conventions are exposed as pure helpers — notificationProgress(item) and
notificationAction(item) (alongside notificationTitle/notificationBody) — so a custom
renderItem can reuse them instead of reaching into item.data by hand:
import { notificationProgress, notificationAction } from '@dudousxd/nestjs-notifications-react';
<Inbox
renderItem={(item, { markAsRead }) => {
const progress = notificationProgress(item); // number | null
const action = notificationAction(item); // { label, url } | null
return (
<MyRow
title={item.data.title as string}
progress={progress}
action={action}
unread={!item.readAt}
onClick={() => markAsRead(item.id)}
/>
);
}}
/>See Live / progress notifications for the producer side that drives the bar.
Other props: title, emptyState, onItemClick, className, and panelClassName. The widget ships
with minimal inline styles and no CSS framework dependency, so it inherits your app's look and you
restyle via the class hooks.
SSR-safe. The hooks only touch EventSource/window inside effects and guard for their
presence, so the package renders fine under Next.js / Remix server rendering — the live connection
opens on the client after hydration.
Auth
The client passes through whatever your API needs. Use cookies with credentials: 'include', or send
a bearer token via headers:
<NotificationsProvider
clientOptions={{
baseUrl: '/notifications',
headers: { Authorization: `Bearer ${token}` },
}}
sseUrl="/notifications/stream"
>The hand-written NotificationsClient mirrors the REST controller
exactly. Prefer to generate it? See Typed client with codegen — nestjs-codegen
emits a typed client straight from your controller, so it can't drift from your routes.
In-app notifications
Build an in-app feed from persisted notifications. Inject NotificationsQueryService to list, count, and mark read — or mount the optional REST controller in one call.
Headless SDK & TanStack Query
A framework-neutral client for the inbox API + SSE — fetch functions, a live subscribe(), and TanStack Query option factories that work in React and Vue. For frontends separate from the backend.