Frontend Inbox with Hooks
Build a production inbox with pagination, scoped filters, mutation actions, and resilient refresh behavior.
Frontend Inbox with Hooks
Drop-in bell first
For most apps, the prebuilt NotificationBell is all you need — it renders the icon, unread badge, and an opt-in popover with the full inbox UI. Reach for hooks only when you need custom layout, filters, or actions beyond what the bell ships with.
import { NotificationBell } from "@wacht/nextjs";
// or "@wacht/react-router" / "@wacht/tanstack-router"
export function HeaderActions() {
return <NotificationBell />;
}NotificationBell accepts className, showBadge, scope (same shape as useNotifications), and an onAction(payload) callback fired when a user clicks an actionable CTA inside a notification.
For more layout control: drop NotificationPopover next to your own trigger, or render NotificationPanel inline (e.g. on a dedicated /notifications page). Both take the same scope + onAction props as the bell.
Building from scratch with useNotifications
useNotifications gives you the core read model and mutation actions for inbox UX. Use it when the drop-in components don't fit.
What the hook provides
- Cursor pagination via
loadMore(). - Filtered list by scope/channels/severity/state.
- Item actions: read/unread, archive, star.
- Bulk actions:
markAllAsRead(),archiveAllRead(). - Stream-assisted freshness via internal
useNotificationStreamwiring.
Inbox composition example
import { useNotifications } from "@wacht/react-router";
export function NotificationsInbox() {
const {
notifications,
loading,
hasMore,
loadMore,
markAsRead,
markAsUnread,
archiveNotification,
starNotification,
markAllAsRead,
archiveAllRead,
refetch,
} = useNotifications({
scope: "all",
is_archived: false,
limit: 25,
severity: "warning",
});
if (loading) return <div>Loading notifications...</div>;
return (
<section>
<div>
<button onClick={() => markAllAsRead()}>Mark all read</button>
<button onClick={() => archiveAllRead()}>Archive all read</button>
<button onClick={() => refetch()}>Refresh</button>
</div>
{notifications.map((n) => (
<article key={n.id}>
<h4>{n.title}</h4>
<p>{n.body}</p>
<button onClick={() => markAsRead(n.id)}>Read</button>
<button onClick={() => markAsUnread(n.id)}>Unread</button>
<button onClick={() => archiveNotification(n.id)}>Archive</button>
<button onClick={() => starNotification(n.id)}>Star</button>
</article>
))}
<button disabled={!hasMore} onClick={() => loadMore()}>
Load more
</button>
</section>
);
}UX guidance
- Mark-as-read on explicit row open, not hover.
- Keep active filters in URL state for shareable support context.
- Render clear empty states for “no data” vs “error”.
- Show mutation progress on action buttons for repeated actions.
State consistency guidance
- Let hook-managed optimistic updates handle immediate UX.
- Call
refetch()after bulk operations when consistency matters. - Treat backend as source of truth after reconnect or tab refocus.
Validation checklist
- Filtered views remain consistent after mutations.
- Bulk actions affect intended filtered set.
- Cursor pagination preserves ordering and no duplicates.
- Error states are actionable and recoverable.