Describe the bug
When a user edits an existing message, the edit is submitted as a new message instead of updating the original. The editedMessage state in MessageComposer is wiped before the edit is submitted.
Root Cause (traced via source code)
In stream-chat core, MessageComposer.subscribeDraftUpdated() subscribes to draft.updated WebSocket events on mount. The handler calls this.initState({ composition: draft }) unconditionally — without checking whether editedMessage is currently set. Inside initState, any composition passed in forces editedMessage to null (via compositionIsDraftResponse check). This creates a race condition:
User triggers "edit message" → composer.editedMessage is set
Stream fires a draft.updated WS event (e.g. for the channel's existing draft)
subscribeDraftUpdated handler calls initState({ composition: draft })
initState sets editedMessage = null
User submits → composer sees no editedMessage, sends as new message
The cleanup effect in MessageComposerProvider also contributes: on unmount it calls createDraft().finally(() => clear()), which can fire draft.updated and retrigger the same path on remount (reproducible in React Strict Mode).
To Reproduce
Set up a channel with an existing draft (or have Stream's draft feature enabled)
Click the edit (pencil) icon on any existing message
Modify the message text
Submit
Expected behavior
The message should be updated (PATCH) — the original message is edited in-place, not a new message sent.
Actual behavior
A new message is created (POST) with the edited text.
Workaround
Disabling drafts via composer.updateConfig({ drafts: { enabled: false } }) in setMessageComposerSetupFunction prevents the buggy subscription from mounting and restores correct edit behavior. However this disables all draft functionality (auto-save, multi-tab sync, load on mount).
Package version
stream-chat-react: 14.1.0
stream-chat-css: 5.16.1
stream-chat-js: 9.43.2
Desktop
OS: macOS
Browser: Chrome
Version: latest
Additional context
The problematic code path (pseudocode):
// MessageComposer.subscribeDraftUpdated()
client.on('draft.updated', (event) => {
const draft = event.draft;
// ❌ Missing guard: if (this.editedMessage) return;
this.initState({ composition: draft }); // wipes editedMessage
});
// MessageComposer.initState()
const editedMessage = compositionIsDraftResponse(composition)
? null // ← always null when a draft is passed
: composition?.editedMessage ?? null;
this.state.next({ editedMessage, ... });
Expected fix: subscribeDraftUpdated should bail out early if this.state.getLatestValue().editedMessage is set — an active edit session must not be interrupted by a draft sync event.
Describe the bug
When a user edits an existing message, the edit is submitted as a new message instead of updating the original. The editedMessage state in MessageComposer is wiped before the edit is submitted.
Root Cause (traced via source code)
In stream-chat core, MessageComposer.subscribeDraftUpdated() subscribes to draft.updated WebSocket events on mount. The handler calls this.initState({ composition: draft }) unconditionally — without checking whether editedMessage is currently set. Inside initState, any composition passed in forces editedMessage to null (via compositionIsDraftResponse check). This creates a race condition:
User triggers "edit message" → composer.editedMessage is set
Stream fires a draft.updated WS event (e.g. for the channel's existing draft)
subscribeDraftUpdated handler calls initState({ composition: draft })
initState sets editedMessage = null
User submits → composer sees no editedMessage, sends as new message
The cleanup effect in MessageComposerProvider also contributes: on unmount it calls createDraft().finally(() => clear()), which can fire draft.updated and retrigger the same path on remount (reproducible in React Strict Mode).
To Reproduce
Set up a channel with an existing draft (or have Stream's draft feature enabled)
Click the edit (pencil) icon on any existing message
Modify the message text
Submit
Expected behavior
The message should be updated (PATCH) — the original message is edited in-place, not a new message sent.
Actual behavior
A new message is created (POST) with the edited text.
Workaround
Disabling drafts via composer.updateConfig({ drafts: { enabled: false } }) in setMessageComposerSetupFunction prevents the buggy subscription from mounting and restores correct edit behavior. However this disables all draft functionality (auto-save, multi-tab sync, load on mount).
Package version
stream-chat-react: 14.1.0
stream-chat-css: 5.16.1
stream-chat-js: 9.43.2
Desktop
OS: macOS
Browser: Chrome
Version: latest
Additional context
The problematic code path (pseudocode):
Expected fix: subscribeDraftUpdated should bail out early if this.state.getLatestValue().editedMessage is set — an active edit session must not be interrupted by a draft sync event.