Overriding components
Every replaceable part of the UI has a key in ComponentsContext. When an SDK component renders a part, it looks up the part's key and renders your component if you set one, or the SDK's own otherwise. WithComponents sets keys for everything below it.
For example, this replaces the date separator between days in every message list, and the send icon everywhere it appears:
import type { PropsWithChildren } from "react";
import { StyleSheet, Text, View } from "react-native";
import Svg, { Path } from "react-native-svg";
import type { StreamChat } from "stream-chat";
import {
Chat,
OverlayProvider,
WithComponents,
} from "stream-chat-react-native";
import type {
ComponentOverrides,
IconProps,
InlineDateSeparatorProps,
} from "stream-chat-react-native";
const PlainDateSeparator = ({ date }: InlineDateSeparatorProps) => (
<View style={styles.separator}>
<Text>{date?.toLocaleDateString()}</Text>
</View>
);
const PaperPlaneIcon = ({
fill,
height,
pathFill,
size,
stroke,
width,
...rest
}: IconProps) => (
<Svg
fill="none"
height={height ?? size}
viewBox="0 0 20 20"
width={width ?? size}
{...rest}
>
<Path
d="M3 10l14-7-5 14-2-6-7-1z"
stroke={stroke ?? pathFill ?? fill ?? "black"}
strokeLinejoin="round"
strokeWidth={1.5}
/>
</Svg>
);
// Declared once, at module scope.
const overrides: ComponentOverrides = {
InlineDateSeparator: PlainDateSeparator,
icons: { SendRight: PaperPlaneIcon },
};
export const App = ({
children,
client,
}: PropsWithChildren<{ client: StreamChat }>) => (
<WithComponents overrides={overrides}>
<OverlayProvider>
<Chat client={client}>{children}</Chat>
</OverlayProvider>
</WithComponents>
);
const styles = StyleSheet.create({
separator: { alignSelf: "center", padding: 4 },
});- A message list renders a date separator above the first message of each day. It reads the
InlineDateSeparatorkey and findsPlainDateSeparator, which receives the day asdate. - The send button reads its icon from the
iconskey and findsPaperPlaneIconunderSendRight. - Every other key keeps the SDK's component.
How overrides merge
WithComponents takes one prop, overrides, an object of type ComponentOverrides. Every key is optional.
- Nested providers merge. The keys in
overridesreplace the same keys from anyWithComponentsabove it, and every other key keeps its outer value. The closest provider wins. iconsmerges icon by icon. Overriding one icon keeps the icons an outer provider set, and the SDK's defaults for the rest.- There is nothing to reset with. No provider drops the overrides from above. To give a part of the app the SDK's own component back, set the key to the default component again, for example
{ InlineDateSeparator }imported fromstream-chat-react-native.
Nesting scopes an override to part of the app:
import { Text } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
WithComponents,
} from "stream-chat-react-native";
import type {
ComponentOverrides,
InlineDateSeparatorProps,
} from "stream-chat-react-native";
const DayOnly = ({ date }: InlineDateSeparatorProps) => (
<Text>{date?.toLocaleDateString()}</Text>
);
const RepliesClosed = () => <Text>Replies are closed.</Text>;
const appOverrides: ComponentOverrides = { InlineDateSeparator: DayOnly };
const supportOverrides: ComponentOverrides = {
SendMessageDisallowedIndicator: RepliesClosed,
};
export const SupportChannel = ({ channel }: { channel: StreamChannel }) => (
<WithComponents overrides={appOverrides}>
{/* This channel keeps DayOnly and replaces the notice too. */}
<WithComponents overrides={supportOverrides}>
<Channel channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
</WithComponents>
</WithComponents>
);Overrides are read once
WithComponents builds its merged value when it mounts and keeps it. A new overrides object passed on a later render is ignored, and so is a change in an outer provider. useComponentsContext() also reads the context once per component. So:
- Declare
overridesat module scope, as above, with components declared at module scope too. - Don't make an override depend on state. A component you set can read state and context like any other, so put the logic inside the component rather than choosing a different component on each render.
- To switch overrides while the app runs, remount the subtree, for example by giving the
WithComponentsakeythat changes. Everything below it mounts again and reads the new value.
Where to put WithComponents
A key takes effect only from a WithComponents above the component that reads it. Most keys are read inside Channel, Thread, ChannelList, ThreadList or ChannelDetails, so a provider around those is enough. A few are read higher up:
| Keys | Read by |
|---|---|
ChatLoadingIndicator |
Chat |
ImageGalleryHeader, ImageGalleryFooter, ImageGalleryGrid, ImageGalleryVideoControls, MessageOverlayBackground, MessageActions |
OverlayProvider |
For app-wide overrides, put WithComponents around OverlayProvider, as the first example does. The image gallery also renders icons, so icon overrides set below OverlayProvider don't reach it.
Reading slots in your own components
useComponentsContext() returns every key, with the overrides in effect merged over the defaults. Every key with a default is defined, so you can render it without a check:
import { Pressable } from "react-native";
import { useComponentsContext, useTheme } from "stream-chat-react-native";
export const SearchButton = ({ onPress }: { onPress: () => void }) => {
const { icons } = useComponentsContext();
const {
theme: { semantics },
} = useTheme();
return (
<Pressable accessibilityLabel="Search" onPress={onPress}>
<icons.Search height={20} stroke={semantics.textPrimary} width={20} />
</Pressable>
);
};A custom component that renders a part through useComponentsContext() shows the app's override of that part, the same way the SDK's components do.
The keys in the optional slots have no default and are undefined until you set them.
Override keys by area
The table groups every key by the part of the screen it renders, and links to the page that covers that part. The defaults are declared in package/src/contexts/componentsContext/defaultComponents.ts in the SDK's repository, and the ComponentOverrides type lists every key with its props in your editor. ComponentsContext covers WithComponents, useComponentsContext() and their types.
| Area | Keys |
|---|---|
| Message list | DateHeader, EmptyStateIndicator, InlineDateSeparator, InlineUnreadIndicator, LoadingErrorIndicator, MessageListLoadingIndicator, MessageSystem, NetworkDownIndicator, ScrollToBottomButton, StickyHeader, TypingIndicator, TypingIndicatorContainer, UnreadMessagesNotification |
| A message | Message, MessageItemView, MessageAuthor, MessageBlocked, MessageBounce, MessageContent, MessageDeleted, MessageError, MessageFooter, MessageHeader, MessagePinnedHeader, MessageReminderHeader, MessageReplies, MessageRepliesAvatars, MessageSavedForLaterHeader, MessageStatus, MessageSwipeContent, MessageTimestamp, Reply, SentToChannelHeader, StreamingMessageView |
| Reactions | MessageReactionPicker, MessageUserReactions, MessageUserReactionsAvatar, MessageUserReactionsItem, ReactionListBottom, ReactionListClustered, ReactionListCountItem, ReactionListItem, ReactionListTop |
| Message menu | MessageActionList, MessageActionListItem, MessageOverlayBackground |
| Attachments | Attachment, AttachmentUploadIndicator, AudioAttachment, CircularProgressIndicator, FileAttachment, FileAttachmentGroup, FileAttachmentIcon, FilePreview, Gallery, Giphy, ImageComponent, ImageLoadingFailedIndicator, ImageLoadingIndicator, MediaUploadProgressOverlay, UnsupportedAttachment, UrlPreview, URLPreviewCompact, VideoThumbnail |
| Image gallery | ImageGalleryFooter, ImageGalleryGrid, ImageGalleryHeader, ImageGalleryVideoControls |
| Message composer | AttachButton, AttachmentUploadPreviewList, AudioAttachmentUploadPreview, AutoCompleteSuggestionHeader, AutoCompleteSuggestionItem, AutoCompleteSuggestionList, CooldownTimer, FileAttachmentUploadPreview, FileUploadInProgressIndicator, FileUploadNotSupportedIndicator, FileUploadRetryIndicator, ImageAttachmentUploadPreview, ImageUploadInProgressIndicator, ImageUploadNotSupportedIndicator, ImageUploadRetryIndicator, InputView, KeyboardCompatibleView, MentionSuggestionItem, MessageComposerLeadingView, MessageComposerTrailingView, MessageInputHeaderView, MessageInputTrailingView, SendButton, SendMessageDisallowedIndicator, StartAudioRecordingButton, StopMessageStreamingButton, VideoAttachmentUploadPreview |
| Voice recording | AudioRecorder, AudioRecordingInProgress, AudioRecordingLockIndicator, AudioRecordingPreview, AudioRecordingWaveform |
| Attachment picker | AttachmentPickerContent, AttachmentPickerSelectionBar, ImageOverlaySelectedComponent |
| Polls | PollAllOptionsContent, PollAnswersListContent, PollButtons, PollHeader, PollOptionFullResultsContent, PollResultsContent |
| Channel list | ChannelDetailsBottomSheet, ChannelDetailsHeader, ChannelListFooterLoadingIndicator, ChannelListHeaderErrorIndicator, ChannelListHeaderNetworkDownIndicator, ChannelListLoadingIndicator, ChannelPreview, ChannelPreviewAvatar, ChannelPreviewLastMessage, ChannelPreviewMessage, ChannelPreviewMessageDeliveryStatus, ChannelPreviewMutedStatus, ChannelPreviewPinnedStatus, ChannelPreviewStatus, ChannelPreviewTitle, ChannelPreviewTypingIndicator, ChannelPreviewUnreadCount, Skeleton |
| Channel details | ChannelAddMembersButton, ChannelAddMembersForm, ChannelAddMembersFormContent, ChannelAddMembersFormHeader, ChannelDetailsActionItem, ChannelDetailsActionsSection, ChannelDetailsContent, ChannelDetailsEditButton, ChannelDetailsMemberSection, ChannelDetailsNavHeader, ChannelDetailsNavigationSection, ChannelDetailsProfile, ChannelEditDetailsForm, ChannelEditDetailsFormContent, ChannelEditDetailsFormHeader, ChannelEditImageSheet, ChannelEditName, ChannelMemberActionsSheet, ChannelMemberItem, ChannelMemberList, FileAttachmentItem, FileAttachmentList, MediaItem, MediaList, PinnedMessageItem, PinnedMessageList, RoleItem, RoleList |
| Thread list | ThreadListComponent, ThreadListEmptyPlaceholder, ThreadListItem, ThreadListItemMessagePreview, ThreadListLoadingIndicator, ThreadListLoadingMoreIndicator, ThreadListUnreadBanner, ThreadMessagePreviewDeliveryStatus |
| Notifications | Notification, NotificationIcon, NotificationList |
| Icons | icons |
EmptyStateIndicator and LoadingErrorIndicator are shared: the channel list renders them too, and Channel and Thread render LoadingErrorIndicator when their first page fails to load. ImageComponent renders the images of attachments, previews and channel details, so a replacement can add caching or a different image library.
Optional slots
These keys have no default. The SDK renders nothing for them, or its built-in part, until you set one:
| Key | Where it renders |
|---|---|
ChatLoadingIndicator |
in place of Chat's children while the offline database opens |
ListHeaderComponent |
above the channel list |
MessageContentTopView, MessageContentBottomView, MessageContentLeadingView, MessageContentTrailingView |
around a message's content, inside its bubble |
MessageSpacer |
last in each message row, outside the bubble |
MessageText |
in place of the default rendering of a message's text |
MessageLocation |
a location shared in a message, which renders nothing without this key |
Input |
in place of the composer's whole row: input, buttons and previews |
PollContent |
in place of the default content of a poll in a message |
CreatePollContent |
in place of the default content of the poll creation screen |
MessageActions |
in place of the layout of the message menu, see Overlays and bottom sheets |
Keys that change nothing
The ComponentOverrides type also lists AttachmentPickerIOSSelectMorePhotos, InputButtons, MessageInputFooterView, MessageInputLeadingView, ReactionListItemWrapper and ShowThreadMessageInChannelButton, but no component reads them, so setting them has no effect. To change the composer's input buttons, override MessageComposerLeadingView, which renders them. MessageList has no key because you render it yourself: see Composition.
Icons
Every icon the SDK renders, from the send arrow and the delivery checkmarks to the mute and pin badges and the attachment picker's glyphs, comes from the icons key. Give it only the icons you want to change: the others keep their defaults.
import Svg, { Path } from "react-native-svg";
import type { ChannelPaginator } from "stream-chat";
import { ChannelList, WithComponents } from "stream-chat-react-native";
import type { ComponentOverrides, IconProps } from "stream-chat-react-native";
const SlashedBellIcon = ({
fill,
height,
pathFill,
size,
stroke,
width,
...rest
}: IconProps) => (
<Svg
fill="none"
height={height ?? size}
viewBox="0 0 20 20"
width={width ?? size}
{...rest}
>
<Path
d="M4 4l12 12M8 16h4M6 8a4 4 0 017.5-2M14 10v2l1.5 2H7"
stroke={stroke ?? pathFill ?? fill ?? "black"}
strokeLinecap="round"
strokeWidth={1.5}
/>
</Svg>
);
const iconOverrides: ComponentOverrides = {
icons: { Mute: SlashedBellIcon },
};
export const ChannelListWithIcons = ({
paginator,
}: {
paginator: ChannelPaginator;
}) => (
<WithComponents overrides={iconOverrides}>
<ChannelList paginator={paginator} />
</WithComponents>
);The mute icon in that subtree, for example on a muted channel's row and in its actions sheet, is now SlashedBellIcon. The names are those of the icon components stream-chat-react-native exports, such as SendRight, ThreadReply, Pin, Mute and Search, and ComponentOverrides["icons"] lists every one. The icon of a file attachment is a separate key, FileAttachmentIcon.
What an icon receives
An icon is a component that takes IconProps: the props of react-native-svg's Svg, plus size, pathFill and pathOpacity.
- Size. The SDK passes
widthandheight, orsize. Use them, so the icon fits the place it renders in. - Color. The SDK passes the color from the theme as
stroke,fillorpathFill, depending on the icon: an outlined glyph usually getsstroke, a filled onefillorpathFill. Read whichever your icon draws with, falling back to the others, asSlashedBellIcondoes, and the icon follows the theme and states such as disabled. - The rest, such as
strokeWidthfrom a button, can be spread on theSvg.
Any component as an icon
An icon doesn't have to be an SVG. An image, or an icon from another library, works as long as it takes IconProps:
import { Image } from "react-native";
import type { IconProps } from "stream-chat-react-native";
export const LogoSendIcon = ({ height, size, width }: IconProps) => (
<Image
source={{ uri: "https://example.com/logo-send.png" }}
style={{ height: height ?? size ?? 20, width: width ?? size ?? 20 }}
/>
);Keep icons light: one may render once per message row.
Inheritance
Icon overrides merge icon by icon across nested WithComponents. Set app-wide icons around OverlayProvider, and override one icon deeper down where a part of the app needs it:
import Svg, { Circle, Rect } from "react-native-svg";
import type { Channel as StreamChannel, StreamChat } from "stream-chat";
import {
Channel,
Chat,
MessageComposer,
MessageList,
OverlayProvider,
WithComponents,
} from "stream-chat-react-native";
import type { ComponentOverrides, IconProps } from "stream-chat-react-native";
const BrandSendIcon = ({ height, size, stroke, width }: IconProps) => (
<Svg height={height ?? size} viewBox="0 0 20 20" width={width ?? size}>
<Circle cx={10} cy={10} fill={stroke ?? "black"} r={7} />
</Svg>
);
const UrgentSendIcon = ({ height, size, stroke, width }: IconProps) => (
<Svg height={height ?? size} viewBox="0 0 20 20" width={width ?? size}>
<Rect fill={stroke ?? "black"} height={14} width={14} x={3} y={3} />
</Svg>
);
const brandIcons: ComponentOverrides = { icons: { SendRight: BrandSendIcon } };
const urgentIcons: ComponentOverrides = {
icons: { SendRight: UrgentSendIcon },
};
export const App = ({
client,
escalations,
}: {
client: StreamChat;
escalations: StreamChannel;
}) => (
<WithComponents overrides={brandIcons}>
<OverlayProvider>
<Chat client={client}>
{/* This channel uses UrgentSendIcon; every other icon is inherited. */}
<WithComponents overrides={urgentIcons}>
<Channel channel={escalations}>
<MessageList />
<MessageComposer />
</Channel>
</WithComponents>
</Chat>
</OverlayProvider>
</WithComponents>
);Many icon colors and sizes can be changed through the theme without replacing the icon. Replace the icon when its shape has to change.
Composing from the default component
The SDK's components are exported, so an override can render the default with different props, or wrap it. ChannelPreviewTitle, for example, takes the channel and an optional title:
import {
ChannelPreviewTitle,
useChannelPreviewDisplayName,
useIsDirectChat,
} from "stream-chat-react-native";
import type {
ChannelPreviewTitleProps,
ComponentOverrides,
} from "stream-chat-react-native";
const HashTitle = (props: ChannelPreviewTitleProps) => {
const displayName = useChannelPreviewDisplayName(props.channel);
const isDirectChat = useIsDirectChat(props.channel);
return (
<ChannelPreviewTitle
{...props}
title={isDirectChat ? displayName : `# ${displayName}`}
/>
);
};
export const titleOverrides: ComponentOverrides = {
ChannelPreviewTitle: HashTitle,
};Forward the props you receive, and change only what you need: the default keeps its styling, its theme keys and the parts it reads from context.
A custom MessageItemView
Message holds the logic of each message in a list, and renders its look through the MessageItemView key. The SDK passes MessageItemView no props. Message puts everything about the message into MessageContext, and the component reads it with useMessageContext(). The parts the default is built from read the same context, so a custom one can compose them:
import { Pressable, StyleSheet, Text } from "react-native";
import {
MessageContent,
MessageFooter,
useMessageContext,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";
const CompactMessage = () => {
const { isMyMessage, message, onLongPress } = useMessageContext();
if (message.type === "deleted") {
return <Text style={styles.deleted}>Message deleted</Text>;
}
return (
<Pressable
onLongPress={(event) => onLongPress({ emitter: "message", event })}
style={[styles.row, isMyMessage && styles.mine]}
>
<Text style={styles.author}>
{message.user?.name ?? message.user?.id}
</Text>
<MessageContent />
<MessageFooter />
</Pressable>
);
};
export const messageOverrides: ComponentOverrides = {
MessageItemView: CompactMessage,
};
const styles = StyleSheet.create({
author: { fontWeight: "600" },
deleted: { fontStyle: "italic", padding: 8 },
mine: { alignSelf: "flex-end" },
row: { maxWidth: "80%", padding: 8 },
});MessageItemView renders every regular message: deleted ones, failed ones, ones with a poll or a quoted message. The default handles all of them. A custom one handles the cases your app has. System messages go to MessageSystem instead, and blocked messages to MessageBlocked.
The message menu opens through onLongPress from MessageContext. MessageContent already calls it on a long press of the bubble, and the Pressable above adds the rest of the row. A custom component that doesn't render MessageContent has to call it itself, or the menu never opens.
Keep a custom message light: it renders once per row of the list. Read what it needs from useMessageContext(), and avoid subscribing each row to state that changes for the whole list, such as the paginator's state.items: a change there re-renders every row. The message parts and their props are on Message UI components, and a full walkthrough is the Custom message UI cookbook.
Composition
Not every change needs a key. Channel and Thread render no list or composer of their own. Besides the keyboard-avoiding view around their children and the attachment picker, they render only the children you give them, so their content is yours to compose: leave a part out, add your own, or change the order.
import { StyleSheet, Text, View } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
const RulesBanner = () => (
<View style={styles.banner}>
<Text>Be kind. Moderators are watching.</Text>
</View>
);
export const ModeratedChannel = ({ channel }: { channel: StreamChannel }) => (
<Channel channel={channel}>
<RulesBanner />
<MessageList />
<MessageComposer />
</Channel>
);
const styles = StyleSheet.create({
banner: { padding: 8 },
});The SDK has no channel header or thread header component: the screen's header belongs to your navigation. See Navigation, and the Channel header UI cookbook for a header that shows the channel's name, avatar and typing state.
Pitfalls
- An
overridesobject that changes.WithComponentsreadsoverridesonce, when it mounts. A later object, or an override chosen from state, is ignored. Declare overrides at module scope, and put changing logic inside the component. - An override below the component that reads it. The image gallery and the message menu's background are rendered by
OverlayProvider, and the loading indicator byChat. Overrides for them, and icon overrides meant for the gallery, go aboveOverlayProvider. - A custom
MessageItemViewthat expects props. It receives none: readuseMessageContext(). - A custom message that never calls
onLongPress. WithoutMessageContent, or your own call toonLongPressfromMessageContext, the message menu doesn't open. - An icon that ignores the props it gets. A hard-coded size or color doesn't follow the theme or the disabled state. Read
width,height,size,stroke,fillandpathFill. - A key from the list of keys that change nothing. Override the component that renders the part instead.
Used in: most Features pages and every UI cookbook, for example Custom message and Custom composer.