Skip to content

fix(perf): stop ConversationList from subscribing to all room state#377

Merged
mremond merged 1 commit intomainfrom
fix/conversation-list-render-loop
May 4, 2026
Merged

fix(perf): stop ConversationList from subscribing to all room state#377
mremond merged 1 commit intomainfrom
fix/conversation-list-render-loop

Conversation

@mremond
Copy link
Copy Markdown
Member

@mremond mremond commented May 4, 2026

Summary

  • ConversationList and ArchiveList called useRoom() but only used setActiveRoom and getRoom from it
  • useRoom() subscribes to ~15 room store values: activeRoom(), activeMessages(), allRooms(), joinedRooms(), roomsWithUnreadCount(), totalMentionsCount(), etc.
  • During background MAM sync on fresh connection, rapid room store updates caused 500+ ConversationList re-renders per second, triggering the render loop detector and half-freezing the app on Linux
  • Replaced useRoom() with direct roomStore.getState() callbacks — both actions are pure store reads with no reactive subscription needed

ConversationList and ArchiveList called useRoom() but only used
setActiveRoom and getRoom from it. useRoom() subscribes to ~15 room
store values (activeRoom, activeMessages, allRooms, joinedRooms,
roomsWithUnreadCount, totalMentionsCount, etc.). During background
MAM sync, rapid room store updates caused 500+ ConversationList
re-renders per second, triggering the render loop detector and
freezing the app on Linux.

Replace useRoom() with direct roomStore.getState() callbacks.
Both actions are store reads with no reactive subscription needed.
@mremond mremond merged commit e98b4c3 into main May 4, 2026
1 check passed
@mremond mremond deleted the fix/conversation-list-render-loop branch May 4, 2026 08:19
@mremond mremond added this to the 0.16.0 milestone May 4, 2026
mremond added a commit that referenced this pull request May 4, 2026
)

Follow-up to
[#377](#377) — extends
the same fix pattern to other components that called heavyweight hooks
for a few actions and ended up subscribed to ~15 store values each
(`activeRoom`, `activeMessages`, `joinedRooms`, `roomsWithUnreadCount`,
`drafts` Map, MAM states, etc.). They re-rendered on every chat/room
store update during background MAM sync — same render-loop pressure that
produced the original Linux freeze.

### SDK

- Add `useRoomActions()` and `useChatActions()` — action-only
counterparts to `useRoom()`/`useChat()` with **zero** store
subscriptions. Actions are read directly via `*Store.getState()` in
stable `useCallback`s.

### Always-mounted components (highest impact)

- **CommandPalette**: split into wrapper + content. Wrapper returns
`null` when closed so the heavy hooks (`useChat`, `useRoom`,
`useRoster`) only subscribe while the palette is visible.
- **InviteToRoomModal**: same wrapper/content split.
- **MemberList** (right sidebar): swap `useChat()` for the existing
focused `useChatActive()`. Only needed `activeConversation`.
- **RoomsList** (main sidebar): replace `useRoom()` with focused
`useRoomStore` selectors for `allRooms` / `activeRoomJid` +
`useRoomActions()` for actions. Push the `drafts` Map subscription down
to per-item via a `useRoomStore` selector inside `RoomItem` (same
pattern as `ConversationItem`), so a draft change in one room only
re-renders that row.
mremond added a commit that referenced this pull request May 4, 2026
…or / ContactItem) (#379)

## Summary

Polish PR following
[#377](#377) and
[#378](#378). Closes
the remaining low-/medium-impact callers identified in the audit so that
no live component pulls a full `useChat()` / `useRoom()` / `useAdmin()`
just for one or two values.

### SDK

- New `useAdminPermissions()` — focused subscription returning `{
isAdmin, hasUserCommands, canManageUser }` only. Avoids the ~15 admin
store subscriptions that `useAdmin()` makes (sessions, command queues,
user/room lists, vhosts, pagination…).
- Extracted `USER_COMMANDS` into a shared `adminCommands.ts` so
`useAdmin` and `useAdminPermissions` share the source of truth.

### App

- **ContactItem** (each row in `ContactList`): swap `useAdmin()` for
`useAdminPermissions()`. Each row previously had a full admin-store
subscription, so any unrelated admin update re-rendered every contact in
the list.
- **ContactSelector** (mounted inside `CreateRoomModal`,
`CreateQuickChatModal`, `InviteToRoomModal`, `RoomMembersModal`): drop
`useChat()` in favour of a focused `useChatStore` selector for
`conversations`.
- **RoomConfigModal**: drop `useAdmin()` in favour of a `useCallback`
wrapper around `client.admin.fetchRoomOptions` via `useXMPP()`. Only
`getRoomOptions` was used.

Tests updated for the new mock surface. Full suite passes (5 999 tests,
0 regressions).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant