import { MessageActions, Search } from "stream-chat-react";Upgrade to v14
stream-chat-react@14 keeps the core chat concepts from v13, but it significantly redesigns component structure, UI override points, and several low-level customization APIs.
If your app uses the default SDK UI with minimal customization, the upgrade is usually straightforward. If you override components, rely on context helper HOCs, customize message input behavior, or theme internal DOM selectors, plan for a focused migration pass.
Quick Migration Checklist
- Upgrade package versions and align
stream-chat. - Replace removed imports and stop importing
MessageActionsorSearchfromstream-chat-react/experimental. - Move UI overrides off
Channeland intoWithComponents/ComponentContext. - Migrate message actions, edit flows, and custom message UIs to the v14
MessageActionsandMessageComposermodel. - Update custom message input code to use the slimmer
MessageInputContextplus dedicated hooks such asuseCooldownRemaining(). - Review attachment, gallery, reaction, poll, thread, and modal customizations for renamed props and components.
- Audit CSS selectors, snapshots, and DOM queries against the new markup.
Import And Entrypoint Changes
The most common direct renames are:
| v13 | v14 |
|---|---|
MessageDeleted | MessageDeletedBubble |
MessageOptions | MessageActions |
ChannelSearch | Search |
MessageNotification | NewMessageNotification |
ScrollToBottomButton | ScrollToLatestMessageButton |
ReactionsListModal | MessageReactionsDetail |
MessageIsThreadReplyInChannelButtonIndicator | MessageAlsoSentInChannelIndicator |
Modal | GlobalModal |
AddCommentForm | AddCommentPrompt |
EndPollDialog | EndPollAlert |
SuggestPollOptionForm | SuggestPollOptionPrompt |
The following APIs were removed and require a redesign rather than a rename:
FixedHeightMessageEditMessageFormEditMessageModalMessageActionsBoxMessageActionsWrapperCustomMessageActionsListQuotedPollQuotedMessagePreviewHeaderButtonWithSubmenu
Suggested replacements for the most common removals:
| Removed API | Alternative approach in v14 |
|---|---|
FixedHeightMessage | provide a custom VirtualMessage built against the current MessageUIComponentProps contract |
EditMessageForm, EditMessageModal | start editing through messageComposer.initState({ composition: message }), clear with messageComposer.clear(), and customize EditedMessagePreview |
MessageActionsBox, MessageActionsWrapper, CustomMessageActionsList | rebuild custom actions around MessageActions, defaultMessageActionSet, messageActionSet, and ContextMenu |
QuotedPoll, Poll.isQuoted | move quoted-poll rendering to the quoted-message layer with QuotedMessage, QuotedMessagePreview, or QuotedMessagePreviewUI |
QuotedMessagePreviewHeader | customize the full quoted preview through QuotedMessagePreview / QuotedMessagePreviewUI instead of composing around a separate header |
ButtonWithSubmenu | rebuild submenu flows with ContextMenu, ContextMenuButton, ContextMenuHeader, and ContextMenuBackButton |
MessageActions and Search are no longer exported from stream-chat-react/experimental. Import them from the main package:
The with*Context HOC wrappers were removed. Replace them with hooks inside function components:
import { useMessageContext } from "stream-chat-react";
const CustomMessage = () => {
const { message } = useMessageContext();
return <div>{message.text}</div>;
};Some low-level helper and icon exports were also removed rather than renamed:
- move emoji-only checks from
isOnlyEmojistocountEmojis()ormessageTextHasEmojisOnly() - replace the removed
useAudioController()hook withuseAudioPlayer() - replace
showMessageActionsBox()/shouldRenderMessageActions()with the v14MessageActionsandmessageActionSetflow - replace removed standalone icons such as
CloseIcon,SendIcon,MicIcon,MessageSentIcon, andMessageDeliveredIconwith the publicIconsset or higher-level components such asSendButtonandMessageStatus - if you relied on
attachmentTypeIconMap, inline your own map or migrate to the newer thread preview components
Examples:
Emoji-only checks:
import { countEmojis, messageTextHasEmojisOnly } from "stream-chat-react";
const emojiOnly = messageTextHasEmojisOnly(message);
const emojiCount = countEmojis(message.text);Message action visibility:
import { MessageActions, defaultMessageActionSet } from "stream-chat-react";
const messageActionSet = defaultMessageActionSet.filter(
({ type }) => type !== "delete",
);
<MessageActions messageActionSet={messageActionSet} />;Removed standalone icons:
import {
IconCrossMedium,
MessageStatus,
SendButton,
useMessageInputContext,
} from "stream-chat-react";
const CustomComposerActions = () => {
const { handleSubmit } = useMessageInputContext();
return (
<>
<button>
<IconCrossMedium />
</button>
<SendButton sendMessage={handleSubmit} />
</>
);
};
const CustomMessageFooter = () => <MessageStatus />;Custom attachment icon maps:
import { IconFileBend, IconImages1Alt, IconVideo } from "stream-chat-react";
const attachmentTypeIconMap = {
file: IconFileBend,
image: IconImages1Alt,
video: IconVideo,
};
const AttachmentTypeIcon =
attachmentTypeIconMap[attachment.type] ?? IconFileBend;
<AttachmentTypeIcon />;Search Changes
ChannelSearch was removed. Search now lives in the main package entrypoint, and ChannelList renders it directly when showChannelSearch is enabled.
Before:
import { ChannelList, ChannelSearch } from "stream-chat-react";
import { Search } from "stream-chat-react/experimental";
<ChannelList
additionalChannelSearchProps={{
placeholder: "Search channels and users...",
searchForChannels: true,
}}
ChannelSearch={ChannelSearch}
showChannelSearch
/>;
<Search />;After:
import { ChannelList, Search, WithComponents } from "stream-chat-react";
const CustomSearch = () => (
<Search
exitSearchOnInputBlur
placeholder="Search channels, messages, and users..."
/>
);
<WithComponents overrides={{ Search: CustomSearch }}>
<ChannelList showChannelSearch />
</WithComponents>;Key changes to account for:
- remove direct
ChannelSearchimports - stop using
ChannelList.additionalChannelSearchProps - stop using the removed
ChannelList.ChannelSearchoverride prop - if you customize search inside
ChannelList, move that work toWithComponents/ComponentContextwithSearch,SearchBar,SearchResults, and related search subcomponents - if you rendered the experimental
Searchdirectly, keep renderingSearch, but import it fromstream-chat-react
Move UI Overrides From Channel To WithComponents
In v13, many UI overrides could be passed directly to Channel. In v14, that forwarded override surface was removed from ChannelProps. Use WithComponents instead.
Before:
import {
Channel,
ChannelHeader,
MessageInput,
MessageList,
Thread,
Window,
} from "stream-chat-react";
// Import your custom override components from your codebase.
<Channel
Input={CustomMessageInput}
Message={CustomMessage}
MessageActions={CustomMessageActions}
Modal={CustomModal}
>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>;After:
import {
Channel,
ChannelHeader,
MessageInput,
MessageList,
Thread,
Window,
WithComponents,
} from "stream-chat-react";
// Import your custom override components from your codebase.
<WithComponents
overrides={{
Input: CustomMessageInput,
Message: CustomMessage,
MessageActions: CustomMessageActions,
Modal: CustomModal,
}}
>
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</WithComponents>;Important override-key changes:
| v13 override key | v14 migration |
|---|---|
MessageNotification | NewMessageNotification |
ReactionsListModal | MessageReactionsDetail |
MessageOptions | MessageActions |
MessageIsThreadReplyInChannelButtonIndicator | MessageAlsoSentInChannelIndicator |
FileUploadIcon | AttachmentSelectorInitiationButtonContents |
EditMessageInput | remove old edit-flow override and customize MessageInput / EditedMessagePreview instead |
EditMessageModal | removed |
QuotedPoll | removed |
Channel also changed its empty-state behavior. If there is no active channel, v14 renders EmptyStateIndicator by default. To preserve the old blank behavior, pass:
import { Channel } from "stream-chat-react";
<Channel EmptyPlaceholder={null}>...</Channel>;Message, Actions, And Edit Flow Changes
MessageOptions has been removed. The supported customization path is the v14 MessageActions component and its action-set model.
Before:
import { MessageOptions } from "stream-chat-react";
const CustomMessageActions = () => <MessageOptions />;After:
import {
MessageActions,
defaultMessageActionSet,
useMessageContext,
useMessageComposer,
} from "stream-chat-react";
const CustomMessageActions = () => {
const actions = defaultMessageActionSet.filter(
({ type }) => type !== "delete",
);
return <MessageActions messageActionSet={actions} />;
};
const CustomMessage = () => {
const { groupedByUser, handleDelete, message } = useMessageContext();
const messageComposer = useMessageComposer();
return (
<div data-grouped-by-user={groupedByUser}>
<button
onClick={() => messageComposer.initState({ composition: message })}
>
Edit
</button>
<button onClick={() => handleDelete({ hardDelete: true })}>Delete</button>
</div>
);
};Key message-level changes:
customMessageActionswas removed fromMessageContext,Message,MessageList, andVirtualizedMessageList.handleDeletechanged from(event, options?)to(options?).- Custom
Messageoverrides should read values such asgroupedByUser,firstOfGroup, andendOfGroupfromuseMessageContext()instead of expecting them as injected component props. MessageDeletedwas replaced byMessageDeletedBubble.MessageEditedTimestampwas removed. UseMessageEditedIndicatorfor the default edited label + tooltip, or render your ownTimestampagainstmessage.message_text_updated_atif you need a fully custom edited-time UI.MessageStatusPropsno longer includeAvatar, and the default status UI no longer renders the old standalone status icons or reader avatar.MessageTextProps.themewas removed, andMessageTextno longer owns quoted-message rendering.MessageTimestampnow defaults to time-only formatting (HH:mm). If you want the old calendar-style default back, overridetimestamp/MessageTimestampinStreami18nor provide a customMessageTimestamp.
Edit-message flows now run through MessageComposer:
- remove
EditMessageForm,EditMessageModal,useEditHandler, andclearEditingState - start editing with
messageComposer.initState({ composition: message }) - cancel or clear the edit state with
messageComposer.clear() - customize the edit preview through
EditedMessagePreview
Message-list floating indicators and unread UI also changed in v14:
MessageNotificationbecameNewMessageNotificationScrollToBottomButtonbecameScrollToLatestMessageButtonMessageListNotificationsis now notifications-only; it no longer receives the old unread-count and scroll-control contract- client-side notifications now render through
NotificationList, whileMessageListNotificationsis the narrower channel-notification plus connection-status container MessageListandVirtualizedMessageListnow renderNewMessageNotificationandScrollToLatestMessageButtondirectly instead of routing them throughMessageListNotificationsUnreadMessagesSeparatornow shows the unread count by default and includes a mark-read button; passshowCount={false}or provide a custom separator if you want the old simpler behavior- the default message action order and action set also changed in v14, so define your own
messageActionSetexplicitly if your UX depends on a stable action menu
Message Input And Composer Changes
v14 keeps MessageInputContext, but it is much smaller. Most composition state now lives in MessageComposer and dedicated hooks.
Before:
import { useMessageInputContext } from "stream-chat-react";
const { cooldownRemaining, handleSubmit, setCooldownRemaining } =
useMessageInputContext();After:
import {
useCooldownRemaining,
useIsCooldownActive,
useMessageInputContext,
} from "stream-chat-react";
const CustomComposerFooter = () => {
const { handleSubmit } = useMessageInputContext();
const cooldownRemaining = useCooldownRemaining();
const isCooldownActive = useIsCooldownActive();
return (
<button disabled={isCooldownActive} onClick={() => void handleSubmit()}>
Send ({cooldownRemaining})
</button>
);
};Key composer changes:
handleSubmitnow accepts only an optional event. It no longer acceptscustomMessageDataorSendMessageOptions.- The default
CooldownTimeris now a zero-prop component:
import { CooldownTimer } from "stream-chat-react";
<CooldownTimer />;- Custom message data should be added through composer middleware or
messageComposer.customDataManager. LinkPreviewListnow shows one preview by default and no longer suppresses previews while quoting a message.- The default textarea now grows up to 10 rows instead of 1. Pass
maxRows={1}if you need the old behavior. AttachmentSelectorwas redesigned. CustomattachmentSelectorActionSetlogic should be reviewed for the new action model, command submenu support, and cooldown-disabled trigger behavior.- the default attachment selector can now add a
selectCommandaction, and runtime-disabled upload actions are filtered out instead of being shown disabled - the default composer now hides the attachment selector and additional composer actions while a slash command is selected; custom input UIs should decide whether to mirror that command-active layout
- Voice recordings moved out of
AttachmentPreviewListinto a dedicatedVoiceRecordingPreviewSlot. QuotedMessagePreviewHeaderwas removed. If you customized quoted previews, overrideQuotedMessagePrevieworQuotedMessagePreviewUIinstead of trying to swap only the old header fragment.AutocompleteSuggestionItemnow follows the redesigned suggestion-list item contract and should not assume the old internal list-item wrapper.
Attachments, Gallery, And File/Media Changes
Attachment customization changed in a few different layers.
At the high level:
AttachmentProps.GallerybecameAttachmentProps.ModalGalleryAttachmentProps.Medianow usesVideoPlayerProps- image and video grouping now flows through the newer media path instead of the old
gallery/imagesplit
If you render ModalGallery directly, migrate from the old images / index API to the new items-based API:
import { ModalGallery } from "stream-chat-react";
<ModalGallery items={galleryItems} />;Gallery itself is no longer the old thumbnail-grid-plus-lightbox component. In v14 it is the provider-style carousel layer, and it only renders visible UI when a GalleryUI is supplied:
import { Gallery, GalleryUI } from "stream-chat-react";
<Gallery GalleryUI={GalleryUI} items={galleryItems} />;If your old customization wrapped Gallery directly, decide whether you actually want:
ModalGalleryfor the old “thumbnail grid that opens a viewer” behaviorGalleryplus a customGalleryUIfor a custom carousel/viewer implementation
If you use lower-level attachment primitives, review these changes too:
MediaContainernow works withattachmentsinstead of a singleattachment- gallery payloads changed from
imagestoitems - audio attachment props changed from
ogtoattachment CardAudiois no longer re-exported from the package root- native
giphyattachments now stay inline throughGiphy; they do not expand throughModalGallery
FileIcon was simplified:
| v13 | v14 |
|---|---|
filename | fileName |
big, size, sizeSmall, type | removed |
mimeTypeToIcon(type, mimeType) | mimeTypeToIcon(mimeType?) |
Reactions, Polls, And Dialogs
The reactions and polls surfaces changed enough that custom wrappers usually need code changes.
Reactions:
- rename
ReactionsListModaltoMessageReactionsDetail - treat
MessageReactionsDetailas dialog content, not as the old standalone modal component - custom
ReactionSelectorcomponents can no longer rely on the old prop surface such asreactionOptions,latest_reactions,reaction_counts, orAvatar - if you customize reaction menu contents, review the current
reactionOptionsand quick/extended action model
Polls:
QuotedPollwas removedPoll.isQuotedwas removedAddCommentForm,EndPollDialog, andSuggestPollOptionFormwere renamed toAddCommentPrompt,EndPollAlert, andSuggestPollOptionPrompt- several poll-dialog override components no longer receive
closecallbacks directly and now depend on dialog context
If you previously rendered quoted polls with QuotedPoll or <Poll isQuoted />, move that UI to the quoted-message layer instead of the poll component itself. The default quoted preview already renders poll messages as a compact summary, so integrations that only need a small quoted-poll preview can usually switch to QuotedMessage or QuotedMessagePreviewUI:
import { QuotedMessagePreviewUI } from "stream-chat-react";
<QuotedMessagePreviewUI quotedMessage={quotedMessage} />;If you need a richer quoted-poll card than the default compact preview, override QuotedMessage or QuotedMessagePreview and render quotedMessage.poll inside your own quoted-message UI.
Dialogs:
Modalwas replaced byGlobalModalButtonWithSubmenuwas removed; submenu-style UIs should be rebuilt aroundContextMenu- the old
.str-chat__modal__innerwrapper no longer exists - some prompt-like override components, such as
MessageBouncePrompt, now close through modal context instead of anonCloseprop RecordingPermissionDeniedNotificationno longer receives anonCloseprop and no longer behaves like the old dismissible notification- custom dialog-style overrides that previously relied on explicit
closecallbacks should be reviewed foruseModalContext()-based dismissal instead
Explicit Query Limits
Channel and ChannelList no longer inject the old initial query limits for you. If your app relied on the previous defaults, set them explicitly:
import { Channel, ChannelList } from "stream-chat-react";
<Channel channelQueryOptions={{ messages: { limit: 20 } }}>
...
</Channel>
<ChannelList options={{ limit: 30 }} />Chat Sidebar State
Use initialNavOpen directly when you need an explicit initial sidebar state:
import { Chat } from "stream-chat-react";
<Chat initialNavOpen={false}>...</Chat>;Header And Thread Changes
ChannelHeader.livewas removed. The default header also no longer renderschannel.data.subtitle.ThreadHeaderno longer acceptsoverrideImage.ThreadHeadersubtitle and close-button behavior changed.- in v13 the SDK only exposed the message-list typing indicator. In v14 typing state is split across two surfaces:
TypingIndicatoris the message-list indicator. In v14 it renders an avatar stack plus animated dots, and it receivesscrollToBottomplus optionalisMessageListScrolledToBottomso it can stay pinned to the latest message while someone is typing.TypingIndicatorHeaderis the header subtitle indicator.ChannelHeadernow switches its subtitle from online-status text toTypingIndicatorHeaderwhen someone is typing in the channel, andThreadHeaderdoes the same for thread typing state.
- custom
TypingIndicatoroverrides only affect the message-list indicator. If you ship a customChannelHeaderorThreadHeaderand want to preserve the new default typing behavior, renderTypingIndicatorHeaderin those custom headers yourself. ChatView.ThreadAdapterno longer stays blank when no thread is selected. It now renders an empty-state placeholder after thread state is ready.- if you want to preserve the old blank thread-pane behavior, wire
ThreadProvidermanually or overrideEmptyStateIndicatorin that scope with a component that rendersnull ChatView.Selectornow defaults to icon-only buttons. PassiconOnly={false}to preserve the old labeled UI:
For example:
import { TypingIndicatorHeader } from "stream-chat-react";
const CustomChannelHeader = () => {
return (
<div>
<div>General</div>
<div>
{hasTyping ? (
<TypingIndicatorHeader />
) : (
<span>{onlineStatusText}</span>
)}
</div>
</div>
);
};
const CustomThreadHeaderSubtitle = () => (
<div>
{hasThreadTyping ? (
<TypingIndicatorHeader threadList />
) : (
<span>
{threadDisplayName} · {replyCount} replies
</span>
)}
</div>
);In the example above, hasTyping, onlineStatusText, hasThreadTyping, threadDisplayName, and replyCount come from your own header logic or the relevant SDK contexts/hooks.
import { ChatView } from "stream-chat-react";
<ChatView.Selector iconOnly={false} />;Smaller Custom-Component Prop Changes To Review
If your app overrides low-level UI pieces, review these prop-surface changes as part of the migration:
| Area | v13 | v14 |
|---|---|---|
Avatarimage, name -> imageUrl, userName, required size | <Avatar image={url} name={name} /> | <Avatar imageUrl={url} size={32} userName={name} /> |
ChannelAvatar / GroupAvatargroupChannelDisplayInfo -> displayMembers, overflowCount, required size | <ChannelAvatar groupChannelDisplayInfo={info} /> | <ChannelAvatar displayMembers={members} overflowCount={rest} size={40} /> |
channel display helpersgetDisplayTitle, getDisplayImage -> useChannelDisplayName(), getChannelDisplayImage() | getDisplayImage(channel) | getChannelDisplayImage(channel) |
TypingIndicatorPropsthreadList? -> scrollToBottom, optional isMessageListScrolledToBottom, threadList | <TypingIndicator threadList /> | <TypingIndicator scrollToBottom={scrollToBottom} threadList /> |
ThreadHeaderPropsoverrideImage, overrideTitle -> overrideTitle only | <ThreadHeader overrideImage={img} overrideTitle="Thread" /> | <ThreadHeader overrideTitle="Thread" /> |
MessageEditedTimestampremoved in favor of MessageEditedIndicator | <MessageEditedTimestamp calendar open={open} /> | <MessageEditedIndicator calendar /> |
MessageStatusPropsinjected Avatar removed | const MyStatus = ({ Avatar, ...props }) => ... | const MyStatus = (props) => ... |
MessageTextPropstheme removed | <MessageText message={message} theme={theme} /> | <MessageText message={message} /> |
PinIndicatorPropst removed; read translations from context instead | ({ message, t }) => t("Pinned") | ({ message }) => { const { t } = useTranslationContext(); ... } |
suggestion UserItemPropsinjected Avatar removed | ({ entity, Avatar }) => <Avatar user={entity} /> | ({ entity }) => <UserContextMenuButton userName={entity.name} /> |
FileIconPropssize and mode props replaced by fileName, mimeType, optional className | <FileIcon filename="a.pdf" size={32} /> | <FileIcon fileName="a.pdf" mimeType="application/pdf" /> |
Two smaller behavior changes are easy to miss during this pass:
useChannelPreviewInfo()now returns stable empty group info instead ofnull/undefined, so any “is this a group channel?” checks based ongroupChannelDisplayInfotruthiness should be revieweduseLatestMessagePreview()now reports nativegiphyattachments astype: "giphy", so custom channel previews should not assume all GIF-like content comes through theimagebranch- custom typing indicators should accept the new scroll-related props if they need to preserve the default “stay pinned to latest message while typing” behavior, and custom headers should render
TypingIndicatorHeaderif they want the new subtitle-level typing state
Styling, DOM, And Snapshot Review
v14 includes a visible markup refresh. If you maintain custom CSS, DOM queries, or visual snapshots, run a manual review over at least these areas:
- channel header markup and sidebar toggle selectors
- message composer wrappers and controls under
str-chat__message-composer* - avatar markup, initials, and online-status badge selectors
- channel preview and thread list item markup, including the move from
aria-selectedtoaria-pressed - message reactions selector/detail markup
- unread separator, new-message notification, and scroll-to-latest button markup
- modal and dialog markup, especially removal of
.str-chat__modal__inner - date separator visuals and any custom
position/unreadassumptions - attachment selector, link preview cards, and drag-and-drop overlay markup
- loading skeletons and send-to-channel checkbox styling
If your theme relies on the old jump-to-latest CSS variables or old internal wrappers, expect to update those selectors. The default DateSeparator also no longer renders the old line-based position variants or the old unread-prefixed label, so provide a custom DateSeparator if your UI still depends on those visuals.
- Quick Migration Checklist
- Import And Entrypoint Changes
- Search Changes
- Move UI Overrides From Channel To WithComponents
- Message, Actions, And Edit Flow Changes
- Message Input And Composer Changes
- Attachments, Gallery, And File/Media Changes
- Reactions, Polls, And Dialogs
- Smaller Custom-Component Prop Changes To Review
- Styling, DOM, And Snapshot Review