Skip to content

🔥 [🐛] MessageList scrolls upward when adding a reaction to a recent message (content-only updates trigger autoscroll) #3670

Description

@flyaflya

Issue

When viewing a channel at (or near) the bottom of the message list, adding a
reaction (e.g. a "like"/love) to a recent message causes the MessageList to
unexpectedly scroll upward, away from the message the user just reacted to. A
common trigger: a new message arrives, the user taps to "like" it, and the list
immediately jumps up (it appears to land around older/unread messages).

The reacting interaction itself uses the SDK's default reaction handler - there
is no custom reaction send/toggle logic. The list is a standard <MessageList />
inside <Channel>.

This appears to be the React Native counterpart of two bugs already reported and
fixed in the sibling SDKs:

Steps to reproduce

Steps to reproduce the behavior:

  1. Open a channel that has enough messages to be scrollable.
  2. Scroll to the bottom (latest messages).
  3. Send several new messages to the channel from another user while the recipient stays in the chatroom.
  4. Recipient adds a reaction to the most recent messages (long-press the message → tap the reaction).
  5. See the list scroll/jump upward instead of staying anchored on the reacted
    message.

Expected behavior

Adding a reaction should not move the scroll position - the list should stay
anchored on the message the user reacted to (the behavior restored by the Android
fix #5280).

Project Related Information

Customization

Click To Expand

The bug reproduces with a standard MessageList. Our Channel configures a
single custom reaction and a custom MessageHeader (for displaying the reaction
count), but the autoscroll trigger is independent of the header - it fires from
the MessageList autoscroll effect re-running on reaction updates.

// Channel + MessageList setup (trimmed to relevant props)
export const reactionData: ReactionData[] = [{ Icon: LoveReaction, type: 'love' }];

<Channel
  channel={channel}
  MessageHeader={MessageHeaderComponent}
  supportedReactions={reactionData}
  messageActions={messageActions}   // copy / quotedReply / edit / flag only
  deletedMessagesVisibilityType="never"
  maxTimeBetweenGroupedMessages={120000}
  getMessageGroupStyle={getMessageGroupStyle}
  forceAlignMessages="left"
>
  <MessageList />
</Channel>

Offline support

  • I have enabled offline support.
  • The feature I'm having does not occur when offline support is disabled. (stripe out if not applicable)

Environment

Click To Expand

package.json:

{
  "name": "ehfixsdk54",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "dependencies": {
    "@aws-amplify/react-native": "^1.1.11-unstable.96fdd13.0",
    "@clerk/clerk-expo": "^2.15.2",
    "@expo/metro-runtime": "~6.1.2",
    "@expo/vector-icons": "^15.0.2",
    "aws-amplify": "^6.15.6",
    "expo": "~54.0.10",
    "expo-router": "~6.0.8",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-native": "0.81.4",
    "react-native-gesture-handler": "~2.28.0",
    "react-native-reanimated": "~4.1.1",
    "react-native-safe-area-context": "~5.6.0",
    "react-native-screens": "~4.16.0",
    "react-native-svg": "15.12.1",
    "react-native-web": "~0.21.0",
    "react-native-worklets": "0.5.1",
    "stream-chat-expo": "^8.12.4",
    "zustand": "^5.0.8"
  }
}

Resolved Stream versions:

  • stream-chat-expo: 8.12.4
  • stream-chat-react-native-core: 8.12.4
  • stream-chat: 9.32.0

New Architecture (Fabric) is enabled (newArchEnabled: true in app.json).

  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • stream-chat-react-native version you're using that has this issue:
    • 8.12.4 (stream-chat-expo 8.12.4)
  • Device/Emulator info:
    • [x ] I am using a physical device
    • OS version: 26.5
    • Device/Emulator: iPhone 12 mini

Additional context

Root-cause analysis

Adding a reaction mutates channel state, which produces a new messages array
reference
. In useMessageList, processedMessageList is a useMemo keyed on
messageList, so it recomputes on any reaction/edit (not just on new messages):

// package/src/components/MessageList/hooks/useMessageList.ts
const processedMessageList = useMemo<LocalMessage[]>(() => {
  // ...
}, [messageList, deletedMessagesVisibilityType, client.userID, isFlashList]);

That recompute re-runs the autoscroll effect in MessageList.tsx. When the list
is already on the latest set, latestNonCurrentMessageBeforeUpdateRef.current is
undefined, so didMergeMessageSetsWithNoUpdates evaluates to false, which
forces setAutoscrollToRecent(true) and recreates maintainVisibleContentPosition
— even though this was a content-only update (a reaction), not a new message:

// package/src/components/MessageList/MessageList.tsx
const didMergeMessageSetsWithNoUpdates =
  latestNonCurrentMessageBeforeUpdate?.id === latestCurrentMessageAfterUpdate.id;
// undefined === <id>  ->  false

const shouldForceScrollToRecent =
  !didMergeMessageSetsWithNoUpdates ||
  processedMessageList.length - messageListLengthBeforeUpdate.current > 0;
// -> true

if ((maximumMessageLimit && shouldForceScrollToRecent) || !maximumMessageLimit) {
  setAutoscrollToRecent(shouldForceScrollToRecent); // -> true on a reaction
}

Because the reaction also changes the rendered height of the message, the
maintainVisibleContentPosition re-anchor (minIndexForVisible: 1) on the New
Architecture FlatList shifts the viewport, producing the upward jump. The effect
cannot currently distinguish a genuine new/removed message from a content-only
update (reaction/edit).

This logic is unchanged on the current develop branch
(MessageList.tsx on develop),
so the latest releases are affected too.

Suggested fix

In the autoscroll effect, skip setAutoscrollToRecent(true) when the update is
content-only — i.e. when the message count is unchanged
(processedMessageList.length === messageListLengthBeforeUpdate.current) and
the latest message id is unchanged. That prevents reactions/edits from triggering
an autoscroll, matching the behavior restored on Android (#5280).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions