In-App Notifications

The SDK provides in-app notifications via a built-in Toaster or your own implementation.

Best Practices

  • Render the toaster near the app root to avoid clipping and z-index issues.
  • Use useInAppNotificationsState as the single source of truth for UI.
  • Keep notifications brief and actionable; avoid stacking too many at once.
  • Set clear severity styles for quick scanning.
  • Close notifications on navigation changes to prevent stale context.

In-app notifications

In this cookbook, we will show you how to use the built-in toaster to display in-app notifications.

Store

The SDK provides a store for in-app notifications, exposed via useInAppNotificationsState.

const { openInAppNotification, closeInAppNotification, notifications } =
  useInAppNotificationsState();

The hook returns:

  • openInAppNotification: Opens a new notification. This can be used to display a notification for a specific action.
  • closeInAppNotification: Closes a notification. Calling this method will remove the notification from the store.
  • notifications: A list of all the notifications. This is a read-only list of the notifications that are currently displayed.

Use this hook to drive your Toaster UI.

Creating a Custom Toaster

Build a custom toaster with useInAppNotificationsState.

An example of a custom toaster is the following:

import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Animated, {
  Easing,
  SlideInDown,
  SlideOutDown,
} 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 severityIconMap: Record<Notification["severity"], string> = {
  error: "❌",
  success: "✅",
  warning: "⚠️",
  info: "ℹ️",
};

export const Toast = () => {
  const { closeInAppNotification, notifications } =
    useInAppNotificationsState();
  const { top } = useSafeAreaInsets();
  const {
    theme: {
      colors: { overlay, white_smoke },
    },
  } = useTheme();

  return (
    <SafeAreaView style={[styles.container, { top }]} pointerEvents="box-none">
      {notifications.map((notification) => (
        <Animated.View
          key={notification.id}
          entering={SlideInDown.easing(Easing.bezierFn(0.25, 0.1, 0.25, 1.0))}
          exiting={SlideOutDown}
          style={[styles.toast, { backgroundColor: overlay }]}
        >
          <View style={[styles.icon, { backgroundColor: overlay }]}>
            <Text style={[styles.iconText, { color: white_smoke }]}>
              {severityIconMap[notification.severity]}
            </Text>
          </View>
          <View style={styles.content}>
            <Text style={[styles.message, { color: white_smoke }]}>
              {notification.message}
            </Text>
          </View>
          <TouchableOpacity
            onPress={() => closeInAppNotification(notification.id)}
          >
            <Text style={[styles.close, { color: white_smoke }]}>✕</Text>
          </TouchableOpacity>
        </Animated.View>
      ))}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    right: 16,
    left: 16,
    alignItems: "flex-end",
  },
  toast: {
    width: width * 0.9,
    borderRadius: 12,
    padding: 12,
    marginBottom: 8,
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    shadowColor: "#000",
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 5,
  },
  content: {
    flex: 1,
    marginHorizontal: 8,
  },
  message: {
    fontSize: 14,
    fontWeight: "600",
  },
  close: {
    fontSize: 16,
  },
  icon: {
    width: 20,
    height: 20,
    borderRadius: 12,
    justifyContent: "center",
    alignItems: "center",
  },
  iconText: {
    fontWeight: "bold",
    includeFontPadding: false,
  },
  warning: {
    backgroundColor: "yellow",
  },
});

This example uses the Notification type from stream-chat.

Triggering a notification

You can trigger the notification by calling the openInAppNotification method with the notification object.

openInAppNotification({
  id: "1",
  message: "This is a test notification",
  severity: "info",
  created_at: Date.now(),
  origin: {
    emitter: "test_emitter",
    id: "test_id",
  },
});

Configuring the Toaster

Render Toast near the top of your app hierarchy.

<Toast />

Closing a Toaster

The toaster will automatically close after a certain amount of time. You can also close the toaster manually by calling the closeInAppNotification method.

Client-side notifications

To listen to client side notifications, you can use the useClientNotifications hook exposed from the SDK.

const { notifications } = useClientNotifications();

This hook returns the following methods:

  • notifications: A list of all the notifications. This is a read-only list of the notifications that are currently displayed.

Compare the notifications list to decide when to show a toast.

In the Sample App, useClientNotificationsHandler listens to client-side notifications and triggers the toaster.

This is how it looks like:

import type { Notification } from "stream-chat";
import {
  useClientNotifications,
  useInAppNotificationsState,
} from "stream-chat-react-native";

import { useEffect, useMemo, useRef } from "react";

export const usePreviousNotifications = (notifications: Notification[]) => {
  const prevNotifications = useRef<Notification[]>(notifications);

  const difference = useMemo(() => {
    const prevIds = new Set(
      prevNotifications.current.map((notification) => notification.id),
    );
    const newIds = new Set(
      notifications.map((notification) => notification.id),
    );
    return {
      added: notifications.filter(
        (notification) => !prevIds.has(notification.id),
      ),
      removed: prevNotifications.current.filter(
        (notification) => !newIds.has(notification.id),
      ),
    };
  }, [notifications]);

  prevNotifications.current = notifications;

  return difference;
};

/**
 * This hook is used to open and close the notifications when the notifications are added or removed.
 * @returns {void}
 */
export const useClientNotificationsHandler = () => {
  const { notifications } = useClientNotifications();
  const { openInAppNotification, closeInAppNotification } =
    useInAppNotificationsState();
  const { added, removed } = usePreviousNotifications(notifications);

  useEffect(() => {
    added.forEach(openInAppNotification);
    removed.forEach((notification) => closeInAppNotification(notification.id));
  }, [added, closeInAppNotification, openInAppNotification, removed]);
};