Presenting new notifications
Open a notification as a popup, bottom sheet or screen when it arrives, pick the screens it may appear on, and show the latest unseen one on app start.
Every notification lands in the inbox. The SDKs can also hand each newly arrived notification to your app, so you can present it however you like: a popup, a bottom sheet, a full screen or a toast. The SDKs decide when a notification is new; your app decides how and where it appears.
How arrivals work
- The first feed the client loads is the baseline: what's already there isn't news (unless you ask for it, see on app start). The cached feed shown before that never counts either.
- After that, every unread, unarchived notification published since the last fetch is an
arrival. Arrivals are detected on each feed fetch: with
realtime they show up within seconds,
with polling within one interval while the app is in the foreground, and otherwise on the
next refetch (returning to the app or
refresh()). - While the inbox is filtered by category or shows the archive, arrivals wait: they are detected once it shows the whole feed again.
- Each arrival goes to one listener, oldest first. A notification that no listener takes yet waits in a queue: until a matching listener subscribes, or until the app is visible again if it arrived in the background.
- A queued notification is dropped once it's read or archived (on any device), once it
expires, when the user opens the inbox (
markAllAsSeen), and onidentify. - In the browser, a notification is presented in one tab only: a hidden tab leaves it to the one the user is looking at.
React and React Native
usePresentedNotification() holds one arrived notification at a time. Render it in any
component you like; dismiss() hides it, marks it read and lets the next queued one in.
import { usePresentedNotification } from "@notificationstream/react-native";
function NotificationSheet() {
const { notification, dismiss } = usePresentedNotification();
return (
<BottomSheet visible={!!notification} onClose={() => dismiss()}>
{notification && (
<>
<Text>{notification.title}</Text>
{notification.body && <Text>{notification.body}</Text>}
</>
)}
</BottomSheet>
);
}Pass dismiss({ markAsRead: false }) to keep it unread in the inbox. For lower-level
control, useNotificationArrival(listener, options) calls listener with each arrival;
return false from it to leave the notification queued.
Choosing the presentation
Tell the app how to present a notification with its category or data,
for example "data": { "display": "bottom_sheet" }:
const { notification, dismiss } = usePresentedNotification();
if (!notification) return null;
switch (notification.data?.display) {
case "screen":
return <AnnouncementScreen item={notification} onClose={() => dismiss()} />;
case "popup":
return <Popup item={notification} onClose={() => dismiss()} />;
default:
return <NotificationBottomSheet item={notification} onClose={() => dismiss()} />;
}Choosing the screens
Mount the hook in the screens (or layouts) where notifications may appear, and use filter
to say which notifications each one takes. A notification no mounted screen takes waits in
the queue until the user reaches a screen that does.
// Send with "data": { "screen": "home" } to show it on the home screen only.
function HomeScreen() {
const isFocused = useIsFocused(); // React Navigation / Expo Router
const { notification, dismiss } = usePresentedNotification({
filter: (item) => item.data?.screen === "home",
enabled: isFocused,
});
// …
}enabledmatters on React Native, where screens in a stack stay mounted in the background. PassuseIsFocused()so only the visible screen presents. On the web, mount the hook in the route component or passenabled: pathname === "/dashboard".- Leave
filterout on a screen that may show anything. To keep notifications away from a screen (checkout, a video call), just don't mount the hook there: they wait until the user leaves. - When several mounted hooks match, the one that subscribed first wins.
On app start
deliverOnStart on the provider (or createNotificationClient) also presents notifications
that already existed when the app started:
| Value | |
|---|---|
"none" (default) | Only notifications that arrive while the app runs. |
"latest" | Also the newest unseen notification. |
"unseen" | Also every unseen notification on the first page, oldest first. |
"Unseen" means not read, not archived and published after the user last opened the inbox.
<NotificationProvider
publishableKey="pk_live_…"
user={{ id: user.id }}
token={getNotificationToken}
deliverOnStart="latest"
>
<App />
<NotificationSheet />
</NotificationProvider>Since dismiss() marks the notification read, it isn't presented again on the next start.
A user switch (identify) counts as a new start.
JavaScript
client.onNotification(listener, { filter }) is the same mechanism without React. It
starts the client like subscribe and returns an unsubscribe function.
const client = createNotificationClient({ publishableKey, user, token, deliverOnStart: "latest" });
// One at a time: stop listening while a dialog is open, the rest stay queued.
function listen() {
const off = client.onNotification((item) => {
off();
showDialog(item, () => {
void client.markAsRead(item.id);
listen();
});
});
}
listen();A listener can also return false to leave a notification queued. It's offered again on the
next fetch or when a listener subscribes.