@quanticjs/react-notifications
v8.0.0
Published
QuanticJS notification center — provider, hooks, and NotificationBell component
Downloads
827
Readme
@quanticjs/react-notifications
Notification center for QuanticJS apps — a standardized backend contract, the
full hook set, and a drop-in NotificationBell component (badge + dropdown
panel). Uses the app's ApiClient from QuanticProvider, so CSRF, auth
refresh, and correlation IDs come for free.
Install
pnpm add @quanticjs/react-notificationsPeer dependencies: react, @quanticjs/react-core@^7,
@quanticjs/react-query@^7, @quanticjs/react-ui@^7,
@tanstack/react-query@^5.
Requires @quanticjs/tailwind-preset >= 8 in the consuming app's CSS build — components render with v8 token utilities (shadow-* tiers, z-(--z-*), animate-*) that compile to nothing on older presets. See docs/MIGRATION-8.md.
Quick start
import { QuanticProvider, createDefaultClient } from '@quanticjs/react-core';
import { QuanticQueryProvider } from '@quanticjs/react-query';
import { NotificationsProvider, NotificationBell } from '@quanticjs/react-notifications';
const client = createDefaultClient({ baseUrl: '/api' });
function App() {
return (
<QuanticProvider client={client}>
<QuanticQueryProvider>
<NotificationsProvider onNavigate={(n) => n.link && router.push(n.link)}>
<Header right={<NotificationBell viewAllHref="/notifications" />} />
</NotificationsProvider>
</QuanticQueryProvider>
</QuanticProvider>
);
}Endpoint contract
The default fetchers expect these endpoints under apiBasePath
(default /notifications):
| Operation | Request | Response |
|---|---|---|
| List | GET {base}?page&limit | { data: NotificationDto[]; total: number } |
| Unread count | GET {base}/unread-count | { count: number } |
| Mark read | POST {base}/{id}/read | — (idempotent) |
| Mark all read | POST {base}/read-all | — |
interface NotificationDto {
id: string;
title: string;
body?: string;
read: boolean;
createdAt: string; // ISO 8601
link?: string; // app-relative navigation target
type?: string; // app-defined category
metadata?: Record<string, unknown>;
}Backends that don't match the contract override individual operations via
fetchers — each receives the app's ApiClient:
<NotificationsProvider
fetchers={{
list: async (client, { page, limit }) => {
const res = await client.get<LegacyShape>('/inbox', { params: { p: page, n: limit } });
return { data: res.items.map(toNotificationDto), total: res.totalCount };
},
}}
>Provider config
| Prop | Default | Description |
|---|---|---|
| apiBasePath | /notifications | Base path for the standard endpoints |
| pollIntervalMs | 60000 | Unread-count poll interval; 0 disables |
| onNavigate | — | Called with the NotificationDto when an item is clicked |
| fetchers | — | Per-operation overrides (see above) |
Polling uses TanStack Query's refetchInterval plus
refetchOnWindowFocus: true, so a backgrounded tab pauses polling and
refreshes on return — keep the interval modest to be battery-friendly on
mobile.
Hooks
useNotifications({ page?, limit? })— paginated list (defaults1/20)useUnreadCount()— count from the dedicated endpoint (never derived from the visible list), polled per provider configuseMarkRead()/useMarkAllRead()— mutations with optimistic updates of the list and count caches, rolled back on error. No built-in toast: the mutation'serroris returned for the app to surface.
All hooks throw a descriptive error outside <NotificationsProvider>.
NotificationBell
Bell button with an unread badge (capped at 99+; the aria-label includes
the count) and a popover panel showing the latest notifications with
"Mark all read", relative timestamps, and unread indicators. Escape and
outside clicks close it; focus moves into the panel and returns to the bell
on close.
| Prop | Default | Description |
|---|---|---|
| maxItems | 10 | Notifications shown in the panel |
| viewAllHref | — | Footer link target (footer omitted when unset) |
| renderLink | <a> | Custom footer link renderer (e.g. router <Link>) |
| labels | English | Override any string, incl. bellAriaLabel(count), for i18n |
| className / panelClassName | — | Styling hooks |
NotificationPanel is exported separately for custom triggers.
Internationalization
Beyond the labels prop, strings resolve app-wide from the TranslationProvider in @quanticjs/react-ui — this package registers the notifications namespace (Partial<NotificationLabels>). Precedence per key: explicit labels prop > provider catalog > English default.
import { TranslationProvider } from '@quanticjs/react-ui';
<TranslationProvider
locale="de-DE"
translations={{
notifications: {
panelTitle: 'Benachrichtigungen',
bellAriaLabel: (count) => `Benachrichtigungen (${count} ungelesen)`,
},
}}
>
<NotificationBell />
</TranslationProvider>The provider's locale also drives the relative timestamps (formatRelativeTime) reactively. useNotificationLabels(overrides?) is exported for custom notification surfaces that should follow the same resolution.
Cache contract (websocket invalidation)
Query keys are part of the public API and covered by tests:
['notifications', 'list', page, limit]['notifications', 'unread-count']
External systems refresh everything by invalidating the root key. With a websocket bridge:
import { useQueryClient } from '@tanstack/react-query';
import { notificationKeys } from '@quanticjs/react-notifications';
function useNotificationSocketBridge(socket: WebSocket) {
const queryClient = useQueryClient();
useEffect(() => {
const onMessage = (event: MessageEvent) => {
if (JSON.parse(event.data).type === 'notification') {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
}
};
socket.addEventListener('message', onMessage);
return () => socket.removeEventListener('message', onMessage);
}, [socket, queryClient]);
}Apps with push channels can set pollIntervalMs: 0 and rely entirely on
invalidation.
RTL & reduced motion
The bell badge offset and panel anchor use logical properties (-end-0.5, end-0) and notification items align with text-start, so the whole notification center mirrors under dir="rtl" with no configuration. Transitions respect prefers-reduced-motion via the global block in @quanticjs/tailwind-preset's theme.css.
