Performance
A message re-renders only when something it shows changes. Each row subscribes to its own message by id, so a send, a status tick, a reaction or a streamed token renders that one row (plus a neighbour only when the bubbles' grouping changes) and leaves the list and the composer alone. Everything the rows share - render props, styles, actions, labels - reaches them through context, kept stable between renders:
- Event handlers can be inline.
onPressMessage={() => ...},reply={{ onPress: ... }}, an action'sonPress: they are called through a stable proxy that always reaches your latest handler. - Style and config objects can be inline.
timeTextStyle={{ left: { color } }},reactions={{ isEnabled: true }}are compared by content. - Memoize render functions. A new
renderBubblemay draw something new, so a new reference re-renders every row - wrap it inuseCallback.
// ✅ Handlers and objects inline; the render function memoized
const renderBubble = useCallback(props => <MyBubble {...props} />, [])
<Chat renderBubble={renderBubble} onPressMessage={message => open(message)} timeTextStyle={{ left: { color: 'gray' } }} />
Messages, neighbours, context props and virtualization tuning
Keep messages immutable. Update a message by replacing its object (e.g. Chat.append(...), or messages.map(m => m._id === id ? { ...m, sent: true } : m)), never by mutating it in place. A store that rebuilds every message object on each update still only re-renders what changed - unchanged messages are compared by content.
Neighbours are for grouping. previousMessage / nextMessage handed to renderMessage, renderBubble or renderAvatar are kept current for what decides grouping (sender, day, time apart, whether there is one). A row does not re-render because a neighbour's text, status or reactions changed, so do not draw those from a neighbour.
Theme, icons and labels don't need drilling. theme / darkTheme, icons, and labels are read from context (useTheme, useIcons, useLabels), so passing them once on <Chat> is enough; they don't cause per-row churn.
Tune virtualization if needed. The list is Legend List, which keeps only what is near the viewport mounted. drawDistance (in pixels) sets how far beyond the screen it renders; raise it if fast flings show blank space, lower it to mount less. Leave recycleItems off: message rows hold state (players, animations) that must not move to another message.
<Chat listProps={{ drawDistance: 500 }} {...props} />