import { useInAppNotificationsState } from "stream-chat-react-native";
const { openInAppNotification, closeInAppNotification, notifications } =
useInAppNotificationsState();In-App Notifications
The SDK ships with a notification system that lets you surface transient messages (errors, confirmations, warnings) inside the app without a native alert or push notification. You can use the built-in hooks directly or build a fully custom toaster UI on top of them.
Best Practices
- Render the toaster near the app root so it sits above all other screens and avoids clipping or z-index issues.
- Use
useInAppNotificationsStateas the single source of truth for the notification list — avoid duplicating it in local state. - Keep notification text brief and actionable; prefer one or two lines.
- Map each
severityto a distinct visual style (color, icon) so users can scan quickly. - Close stale notifications on navigation changes to prevent out-of-context messages.
- Limit the number of visible notifications — stacking too many at once overwhelms users.
Notification Store
The SDK manages notifications through a central store exposed by the useInAppNotificationsState hook.
| Return value | Type | Description |
|---|---|---|
openInAppNotification | (notification: Notification) => void | Adds a notification to the store and displays it. |
closeInAppNotification | (id: string) => void | Removes a notification from the store by its id. |
notifications | Notification[] | Read-only list of notifications currently in the store. |
The Notification type comes from stream-chat and has the following shape:
import type { Notification } from "stream-chat";| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the notification. |
message | string | Text displayed to the user. |
severity | "error" | "success" | "warning" | "info" | Determines the visual treatment. |
created_at | number | Timestamp (epoch ms) when the notification was created. |
origin | { emitter: string; id: string } | Where the notification originated from. |
Triggering a Notification
Call openInAppNotification with a full Notification object:
openInAppNotification({
id: "msg-sent-1",
message: "Message sent successfully",
severity: "success",
created_at: Date.now(),
origin: {
emitter: "message_composer",
id: "send_action",
},
});You can trigger notifications from anywhere in your component tree as long as the component has access to the hook.
Building a Custom Toaster
Below is a production-style toaster that maps each severity to a distinct background color and icon, animates in/out with react-native-reanimated, and respects safe areas.
import {
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import Animated, {
Easing,
SlideInUp,
SlideOutUp,
} from "react-native-reanimated";
import {
SafeAreaView,
useSafeAreaInsets,
} from "react-native-safe-area-context";
import { useInAppNotificationsState, useTheme } from "stream-chat-react-native";
import type { Notification } from "stream-chat";
const { width } = Dimensions.get("window");
const severityConfig: Record<
Notification["severity"],
{ icon: string; backgroundColor: string }
> = {
error: { icon: "✕", backgroundColor: "#FEE2E2" },
success: { icon: "✓", backgroundColor: "#D1FAE5" },
warning: { icon: "!", backgroundColor: "#FEF3C7" },
info: { icon: "i", backgroundColor: "#DBEAFE" },
};
const severityTextColor: Record<Notification["severity"], string> = {
error: "#991B1B",
success: "#065F46",
warning: "#92400E",
info: "#1E40AF",
};
export const Toast = () => {
const { closeInAppNotification, notifications } =
useInAppNotificationsState();
const { top } = useSafeAreaInsets();
const {
theme: {
colors: { black },
},
} = useTheme();
if (notifications.length === 0) {
return null;
}
return (
<SafeAreaView style={[styles.container, { top }]} pointerEvents="box-none">
{notifications.map((notification) => {
const config = severityConfig[notification.severity];
const textColor = severityTextColor[notification.severity];
return (
<Animated.View
key={notification.id}
entering={SlideInUp.duration(300).easing(
Easing.bezierFn(0.25, 0.1, 0.25, 1.0),
)}
exiting={SlideOutUp.duration(200)}
style={[styles.toast, { backgroundColor: config.backgroundColor }]}
>
<View
style={[styles.iconContainer, { backgroundColor: textColor }]}
>
<Text style={styles.iconText}>{config.icon}</Text>
</View>
<View style={styles.content}>
<Text
style={[styles.message, { color: black }]}
numberOfLines={2}
>
{notification.message}
</Text>
</View>
<TouchableOpacity
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
onPress={() => closeInAppNotification(notification.id)}
>
<Text style={[styles.close, { color: textColor }]}>✕</Text>
</TouchableOpacity>
</Animated.View>
);
})}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
alignItems: "center",
zIndex: 9999,
},
toast: {
width: width * 0.92,
borderRadius: 12,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 8,
flexDirection: "row",
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 6,
elevation: 5,
},
iconContainer: {
width: 24,
height: 24,
borderRadius: 12,
justifyContent: "center",
alignItems: "center",
},
iconText: {
color: "#FFFFFF",
fontSize: 14,
fontWeight: "700",
includeFontPadding: false,
},
content: {
flex: 1,
marginHorizontal: 12,
},
message: {
fontSize: 14,
fontWeight: "500",
lineHeight: 20,
},
close: {
fontSize: 18,
fontWeight: "600",
},
});Mounting the Toaster
Render <Toast /> at the top of your app hierarchy — typically alongside your navigation container — so it is always visible regardless of the active screen:
import { NavigationContainer } from "@react-navigation/native";
import { Toast } from "./components/Toast";
const App = () => {
return (
<Chat client={chatClient}>
<NavigationContainer>{/* your screens */}</NavigationContainer>
<Toast />
</Chat>
);
};Dismissing Notifications
Notifications can be dismissed in two ways:
- Manual dismiss — the user taps the close button, which calls
closeInAppNotification(id). - Automatic dismiss — the SDK auto-removes notifications after a timeout.
To dismiss programmatically (for example, on screen change):
import { useEffect } from "react";
import { useInAppNotificationsState } from "stream-chat-react-native";
const useCloseNotificationsOnBlur = (isFocused: boolean) => {
const { closeInAppNotification, notifications } =
useInAppNotificationsState();
useEffect(() => {
if (!isFocused) {
notifications.forEach((n) => closeInAppNotification(n.id));
}
}, [isFocused, closeInAppNotification, notifications]);
};Client-Side Notifications
The SDK also exposes a useClientNotifications hook that listens to notifications emitted by the chat client itself (for example, connection errors or moderation events).
import { useClientNotifications } from "stream-chat-react-native";
const { notifications } = useClientNotifications();notifications is a live, read-only list. Each time the client emits or removes a notification the list updates automatically.
Bridging Client Notifications to the Toaster
The following hook watches the client notification list, detects new and removed entries, and syncs them with the in-app notification store so they appear in your toaster automatically.
import { useEffect, useMemo, useRef } from "react";
import type { Notification } from "stream-chat";
import {
useClientNotifications,
useInAppNotificationsState,
} from "stream-chat-react-native";
const usePreviousNotifications = (notifications: Notification[]) => {
const prevNotifications = useRef<Notification[]>(notifications);
const difference = useMemo(() => {
const prevIds = new Set(prevNotifications.current.map((n) => n.id));
const currentIds = new Set(notifications.map((n) => n.id));
return {
added: notifications.filter((n) => !prevIds.has(n.id)),
removed: prevNotifications.current.filter((n) => !currentIds.has(n.id)),
};
}, [notifications]);
prevNotifications.current = notifications;
return difference;
};
export const useClientNotificationsHandler = () => {
const { notifications } = useClientNotifications();
const { openInAppNotification, closeInAppNotification } =
useInAppNotificationsState();
const { added, removed } = usePreviousNotifications(notifications);
useEffect(() => {
added.forEach(openInAppNotification);
removed.forEach((n) => closeInAppNotification(n.id));
}, [added, closeInAppNotification, openInAppNotification, removed]);
};Call useClientNotificationsHandler() once near the root of your app (for example, inside the same component that renders <Toast />):
const App = () => {
useClientNotificationsHandler();
return (
<Chat client={chatClient}>
<NavigationContainer>{/* screens */}</NavigationContainer>
<Toast />
</Chat>
);
};With this setup, any notification the chat client emits is automatically displayed in your custom toaster and removed when the client clears it.