Skip to content

bug: subscribeDraftUpdated unconditionally resets composer state, breaking edit-message flow when drafts are enabled #3181

@minhth1529

Description

@minhth1529

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstatus:confirmedDescribed issue has been reproduced by the repo maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions