useInbox
useInbox subscribes a component to the inbox. It's a thin binding over core's lazy
inbox singleton — and it deliberately reads from two stores: the global InboxHandle
(which carries the app-wide counter and status, and owns markAllRead) and a
filtered InboxView (which carries this view's channels, items, and unseen).
That's why counter is global — it ignores this call's filter — while unseen is
scoped to it.
import { useInbox } from "@portalsdk/react";
function InboxBadge() {
const { counter } = useInbox();
return <span>{counter}</span>;
}
Params
useInbox(params?) takes the same query shape as inbox.view() in core, plus onItem:
| Field | Type | Notes |
|---|---|---|
channelId |
string |
Scope the whole view (items + entry) to one channel. |
where |
InboxWhere<D> |
Filter items by scalar fields of your item data, plus type, channelId, read, muted. |
onItem |
(item: InboxItem<D>) => void |
Fires once per item arriving after mount. |
import { useInbox } from "@portalsdk/react";
interface AssignmentPayload {
ticketId: string;
}
function AssignmentList() {
const { items, unseen } = useInbox<AssignmentPayload>({
where: { type: { eq: "ticket.assigned" } },
});
return (
<div>
<p>{unseen} unseen</p>
<ul>
{items.map((item) => (
<li key={item.id}>{item.data.ticketId}</li>
))}
</ul>
</div>
);
}
Result
useInbox returns channels, items, counter (global), unseen (scoped to this
view's filter), markAllRead (global, zero-argument), and status.
import { useInbox } from "@portalsdk/react";
function InboxPanel() {
const { channels, items, counter, markAllRead, status } = useInbox();
return (
<div>
<p>
{status} — {counter} unread
</p>
<button onClick={() => markAllRead()}>Mark all read</button>
<ul>
{channels.map((c) => (
<li key={c.id}>
{c.name ?? c.id}: {c.unread}
</li>
))}
</ul>
<ul>
{items.map((item) => (
<li key={item.id}>{item.title ?? item.type}</li>
))}
</ul>
</div>
);
}
Reacting to arrival with onItem
items is the accumulated, reactive list — right for rendering a panel. For a one-shot
reaction to a new item arriving (a toast, a sound, an analytics call), use onItem instead
of diffing items yourself:
import { useInbox } from "@portalsdk/react";
function InboxToaster() {
useInbox({
onItem: (item) => {
if (item.type === "ticket.assigned") showToast(item.title ?? "New assignment");
},
});
return null;
}
function showToast(_message: string) {}
Firing semantics, inherited from the underlying core event:
-
Never fires for the items already present when the inbox becomes ready — only for a
genuinely new arrival after that. Render the initial backlog from
itemsinstead. -
Never fires twice for the same item — a redelivered item updates its data in
itemsbut doesn't re-announce itself.item.idis the notification's idempotency key, so this falls out of the same guarantee. - Stable-ref: passing a fresh inline callback on every render doesn't drop a subsequently-arriving item and doesn't re-subscribe.
See In-app notifications for this pattern applied to a full toast component with dismiss handling.