import { useEffect, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { Channel as StreamChatChannel } from "stream-chat";
import {
BadgeNotification,
ChannelAvatar,
ChevronLeft,
useChannelMemberCount,
useChannelOnlineMemberCount,
useChannelPreviewDisplayName,
useChatContext,
} from "stream-chat-react-native";
// Tracks the unread count across the user's other channels and keeps it in sync.
const useTotalUnreadCount = () => {
const { client } = useChatContext();
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const listener = client.on((e) => {
const event = e.me ?? e;
if (event.total_unread_count !== undefined) {
setUnreadCount(event.total_unread_count);
}
});
return () => listener.unsubscribe();
}, [client]);
return unreadCount;
};
const ChannelHeader = ({ channel }: { channel: StreamChatChannel }) => {
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const displayName = useChannelPreviewDisplayName(channel);
const { isOnline } = useChatContext();
const unreadCount = useTotalUnreadCount();
const memberCount = useChannelMemberCount(channel);
const onlineCount = useChannelOnlineMemberCount(channel);
return (
<View style={[styles.container, { marginTop: insets.top }]}>
<Pressable
accessibilityLabel="Back"
accessibilityRole="button"
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<ChevronLeft height={24} width={24} stroke="#080707" />
{unreadCount > 0 ? (
<View style={styles.unreadBadge}>
<BadgeNotification count={unreadCount} size="sm" type="primary" />
</View>
) : null}
</Pressable>
<ChannelAvatar channel={channel} />
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={1}>
{displayName}
</Text>
<Text style={styles.status}>
{isOnline
? `${memberCount} members, ${onlineCount} online`
: "Reconnecting…"}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
alignItems: "center",
borderBottomColor: "#E9EAEA",
borderBottomWidth: 1,
flexDirection: "row",
gap: 8,
paddingHorizontal: 12,
paddingVertical: 8,
},
backButton: {
alignItems: "center",
height: 40,
justifyContent: "center",
width: 40,
},
unreadBadge: {
left: 24,
position: "absolute",
top: 2,
},
titleContainer: {
flex: 1,
},
title: {
fontSize: 16,
fontWeight: "600",
},
status: {
fontSize: 12,
},
});Channel Header
The SDK does not render a header inside the Channel component for you — it's up to you to provide it. This cookbook builds a channel header that shows the channel name and image, a member/online count, a back button with an unread badge, and a connection status, all from public APIs.
Best Practices
- Render the header as a child of
<Channel>, before theMessageList, so it sits at the top of the screen and shares the same channel context as the rest of the UI. - Read channel state through the SDK hooks and components (
useChannelPreviewDisplayName,ChannelAvatar,useChannelMemberCount,useChannelOnlineMemberCount) instead of reaching intochannel.datadirectly — they stay in sync as the channel updates and handle group/1:1 fallbacks for you. - Keep unread and connection state reactive: subscribe to client events (or use the provided hooks) rather than reading a one-time snapshot, so the badge and status update live.
- Apply the top safe-area inset as the header's top margin so it isn't hidden behind the status bar or notch — read it from
useSafeAreaInsets()(react-native-safe-area-context).
Building the Header
Each piece of the header comes from a hook or component you can use directly:
- Name —
useChannelPreviewDisplayName(channel)returns a ready-to-show name: the channel's name when set, the other user's name for 1:1 chats, or a"Alice, Bob and 2 others"style label for groups. - Image —
ChannelAvatarrenders the channel image with built-in fallbacks (channel image, grouped member avatars, or the other user's avatar). - Member/online count —
useChannelMemberCount(channel)anduseChannelOnlineMemberCount(channel)give you live counts for the subtitle. - Back button — wrap the
ChevronLefticon (exported fromstream-chat-react-native) in aPressableand callnavigation.goBack()from React Navigation. - Unread badge —
BadgeNotificationshows the unread count from the user's other channels (total_unread_count), overlaid on the back button so it stays visible after navigating away. A small hook keeps it in sync by listening to client events. - Connection status —
useChatContext()exposesisOnline; use it to show aReconnecting…state when the connection drops.
Adding the Header to the Channel
Place the header inside <Channel>, above the MessageList:
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
const ChannelScreen = ({ channel }) => (
<Channel channel={channel}>
<ChannelHeader channel={channel} />
<MessageList />
<MessageComposer />
</Channel>
);