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 the MessageList, 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 into channel.data directly — 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:

  • NameuseChannelPreviewDisplayName(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.
  • ImageChannelAvatar renders the channel image with built-in fallbacks (channel image, grouped member avatars, or the other user's avatar).
  • Member/online countuseChannelMemberCount(channel) and useChannelOnlineMemberCount(channel) give you live counts for the subtitle.
  • Back button — wrap the ChevronLeft icon (exported from stream-chat-react-native) in a Pressable and call navigation.goBack() from React Navigation.
  • Unread badgeBadgeNotification shows 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 statususeChatContext() exposes isOnline; use it to show a Reconnecting… state when the connection drops.
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,
  },
});

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>
);