Aviary
Recipes

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

The 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:

AppHeader.tsx
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:

main.tsx
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:

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

useNotifications
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, 0100) 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 codegennestjs-codegen emits a typed client straight from your controller, so it can't drift from your routes.

On this page