import {
Channel,
MessageList,
MessageInput,
NotificationList,
} from "stream-chat-react-native";
export const ChannelScreen = () => (
<Channel channel={channel}>
<MessageList />
<MessageInput />
<NotificationList panel="channel" verticalAlignment="bottom" />
</Channel>
);In-App Notifications
The SDK emits in-app notifications for built-in actions (delete a message, vote on a poll, upload an attachment, and so on) and renders them as snackbars via NotificationList. You can also emit notifications from your own components with useNotificationApi, and read the notification stream with useNotifications.
This cookbook covers emitting and reading. For visual customization (theme, overrides, custom snackbar), see the Snackbar cookbook. For concepts (severity, panels, lifecycle), see the In-App Notifications guide.
Best Practices
- Mount
NotificationListinside each panel you want to surface notifications in (MessageList,ChannelList,ThreadList), not at the app root, so the snackbar appears in context. - Always set an
origin.emitterthat identifies the component or feature emitting the notification. Consumers (analytics, tests, custom filters) rely on it. - Use
incidentinstead of writing thetypestring by hand. The SDK buildsdomain:entity:operation:statusfrom the incident so types stay consistent. - Keep messages short: one line, ideally under 60 characters.
- Use
addSystemNotificationfor anything that shouldn't appear in the snackbar (analytics, dev warnings, background errors).
Mounting NotificationList
MessageList, ChannelList, and ThreadList mount a NotificationList for you by default. You don't need to mount one yourself to see the SDK's built-in notifications. If you've replaced one of those components or are building a fully custom layout, mount the snackbar host explicitly:
When mounted inside the SDK's notification target context, the panel prop is optional because NotificationList resolves the target from the surrounding provider.
Emitting a Notification
useNotificationApi returns the emit/remove helpers. Pass an AddNotificationPayload (which always contains message and origin.emitter) and an optional second argument with extras like incident or targetPanels.
import { useNotificationApi } from "stream-chat-react-native";
export const SaveDraftButton = () => {
const { addNotification } = useNotificationApi();
const handlePress = async () => {
try {
await saveDraft();
addNotification({
message: "Draft saved",
origin: { emitter: "SaveDraftButton" },
options: { severity: "success" },
});
} catch (error) {
addNotification(
{
message: "Failed to save draft",
origin: { emitter: "SaveDraftButton" },
options: {
severity: "error",
originalError: error instanceof Error ? error : undefined,
},
},
{
incident: { domain: "api", entity: "draft", operation: "save" },
},
);
}
};
return <Button onPress={handlePress} title="Save draft" />;
};The SDK auto-routes this notification to whichever panel the component is mounted in (channel, thread, channel list, or thread list).
Adding Action Buttons
A notification can carry up to a few action buttons. Each action is { label, handler }. When at least one action is present, the dismiss timer is automatically extended to a minimum of 5 seconds so the user has time to tap.
const { addNotification } = useNotificationApi();
addNotification({
message: "Message deleted",
origin: { emitter: "MessageActions" },
options: {
severity: "success",
actions: [
{
label: "Undo",
handler: () => restoreMessage(message),
},
],
},
});Persistent vs Transient
By default, a notification auto-dismisses after the severity's configured duration (3 seconds for error, warning, success, and info). Set duration: 0 to keep it visible until the user, or your code, dismisses it. Notifications without a resolved duration are persistent too. The snackbar shows a close button on persistent notifications automatically.
addNotification({
message: "Tap to retry the connection",
origin: { emitter: "ConnectionBanner" },
options: {
duration: 0,
severity: "warning",
},
});Persistent notifications take priority over transient ones. If both are present, the snackbar shows the persistent one.
Generating Types from Incidents
The type field categorizes a notification for analytics, dedupe, and translation. Instead of writing the string by hand, describe the incident and the SDK builds the type for you:
addNotification(
{
message: "Failed to send message",
origin: { emitter: "MessageComposer" },
options: { severity: "error" },
},
{
incident: { domain: "api", entity: "message", operation: "send" },
// → type: 'api:message:send:failed' (status inferred from severity='error')
},
);status defaults to 'failed' for error severity, 'success' for success severity, otherwise the severity value itself. Pass incident.status explicitly to override.
Targeting a Specific Panel
By default, the SDK uses the nearest NotificationTargetProvider to resolve the target. The built-in Channel, Thread, ChannelList, and ThreadList components install that provider for you. Override with targetPanels to fan out a notification to specific panels regardless of where the call comes from:
addNotification(
{
message: "Channel pinned",
origin: { emitter: "ChannelListItem" },
options: { severity: "success" },
},
{
targetPanels: ["channel-list"],
},
);Pass multiple panels to broadcast a notification to several at once.
Routing Notifications from a Background Action
When a network call starts inside a channel but completes after the user has navigated away, for example, the user pins a channel from the channel list, you want the resulting notification to land back in the original panel, not the panel that's active when the call resolves.
runWithNotificationTarget registers an "action target" for the duration of the callback. Any notification emitted during the callback is routed to that target, even if the user has moved on:
import {
getChannelNotificationHostId,
useNotificationApi,
} from "stream-chat-react-native";
import type { Channel as StreamChannel } from "stream-chat";
const { addNotification, runWithNotificationTarget } = useNotificationApi();
const handlePin = async (channel: StreamChannel) => {
await runWithNotificationTarget(
async () => {
try {
await channel.pin();
addNotification({
message: "Channel pinned",
origin: { emitter: "ChannelListItem" },
options: { severity: "success" },
});
} catch (error) {
addNotification({
message: "Failed to pin channel",
origin: { emitter: "ChannelListItem" },
options: { severity: "error" },
});
}
},
{ hostId: getChannelNotificationHostId(channel.cid), panel: "channel" },
);
};The SDK uses this pattern internally for message actions, channel actions, and poll mutations.
Reading Notifications
To react to notifications elsewhere in your app, for example, to forward errors to your analytics pipeline, call useNotifications:
import { useEffect } from "react";
import { useNotifications } from "stream-chat-react-native";
export const useAnalyticsLogger = () => {
const notifications = useNotifications({
filter: (n) => n.severity === "error",
});
useEffect(() => {
const last = notifications[notifications.length - 1];
if (!last) return;
analytics.track("chat_error", {
emitter: last.origin.emitter,
type: last.type,
message: last.message,
});
}, [notifications]);
};Without options, useNotifications returns every notification in the store. Pass an explicit target to read notifications for a specific channel, thread, channel list, or thread list.
System Notifications
Some notifications shouldn't appear in the snackbar, such as analytics, internal warnings, or background sync errors. Emit them with addSystemNotification:
const { addSystemNotification } = useNotificationApi();
addSystemNotification({
message: "Background sync failed",
origin: { emitter: "OfflineSync" },
options: { severity: "warning" },
});Read them with useSystemNotifications. They never reach NotificationList.
Dismissing Programmatically
Call removeNotification(id) to dismiss a specific notification. addSystemNotification returns its id; addNotification does not, so capture the id from the notification store if you need to dismiss a snackbar notification.
const { addSystemNotification, removeNotification } = useNotificationApi();
const id = addSystemNotification({
message: "Reconnecting…",
origin: { emitter: "OfflineSync" },
options: { severity: "loading", duration: 0 },
});
await reconnect();
removeNotification(id);To clear every notification in the current target provider, useful when the user navigates away, call removeNotificationsForCurrentPanel. NotificationList already does this when it unmounts, so you only need it for custom layouts.
Migration from useInAppNotificationsState
If you previously rolled your own toaster on top of useInAppNotificationsState, the migration is small but mechanical:
| Old | New |
|---|---|
openInAppNotification({ id, message, severity, … }) | addNotification({ message, origin, options: { severity, … } }) |
closeInAppNotification(id) | removeNotification(id) |
Custom <Toast /> at app root | <NotificationList /> inside each panel, or remove yours when using the built-in hosts |
useClientNotifications().notifications | useNotifications() |
You can delete your custom toaster component in the common case. MessageList, ChannelList, and ThreadList mount NotificationList for you, while Channel and Thread provide the target context around their message lists.
- Best Practices
- Mounting NotificationList
- Emitting a Notification
- Adding Action Buttons
- Persistent vs Transient
- Generating Types from Incidents
- Targeting a Specific Panel
- Routing Notifications from a Background Action
- Reading Notifications
- System Notifications
- Dismissing Programmatically
- Migration from useInAppNotificationsState