This is beta documentation for Stream Chat React Native SDK v9. For the latest stable version, see the latest version (v8) .

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 useInAppNotificationsState as 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 severity to 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.

import { useInAppNotificationsState } from "stream-chat-react-native";

const { openInAppNotification, closeInAppNotification, notifications } =
  useInAppNotificationsState();
Return valueTypeDescription
openInAppNotification(notification: Notification) => voidAdds a notification to the store and displays it.
closeInAppNotification(id: string) => voidRemoves a notification from the store by its id.
notificationsNotification[]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";
FieldTypeDescription
idstringUnique identifier for the notification.
messagestringText displayed to the user.
severity"error" | "success" | "warning" | "info"Determines the visual treatment.
created_atnumberTimestamp (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>
  );
};
Placing `Toast` **after** the navigation container ensures it renders on top of every screen.

Dismissing Notifications

Notifications can be dismissed in two ways:

  1. Manual dismiss — the user taps the close button, which calls closeInAppNotification(id).
  2. 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.

`useClientNotifications` gives you **raw** client events. To bridge them into the toaster, compare the current list against the previous render to detect additions and removals — then forward those into `openInAppNotification` / `closeInAppNotification`.

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.