Skip to content

fix(perf): cut over-subscription in CommandPalette and room modals#378

Merged
mremond merged 2 commits intomainfrom
fix/over-subscription-perf
May 4, 2026
Merged

fix(perf): cut over-subscription in CommandPalette and room modals#378
mremond merged 2 commits intomainfrom
fix/over-subscription-perf

Conversation

@mremond
Copy link
Copy Markdown
Member

@mremond mremond commented May 4, 2026

Summary

Follow-up to #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 useCallbacks.

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.

Conditionally-mounted modals

  • RoomMembersModal, RoomHatsModal, JoinRoomModal, CreateQuickChatModal, OccupantPanel: swap useRoom() for useRoomActions().
  • CreateRoomModal: useRoomActions() for actions + focused useAdminStore subscription for mucServiceJid.

Tests updated to mock useRoomActions where applicable. Full SDK + app suite passes (5 999 tests, 0 regressions).

mremond added 2 commits May 4, 2026 11:58
Several components were calling useChat()/useRoom() but only using a
small subset of actions. These hooks subscribe to ~15 store values
each (activeRoom, activeMessages, joinedRooms, roomsWithUnreadCount,
totalUnreadCount, drafts Map, MAM states, etc.), so the consumers
re-rendered on every chat/room store update during background MAM
sync — feeding the same render-loop pressure that caused the
ConversationList freeze on Linux.

SDK
- Add useRoomActions() and useChatActions(): action-only counterparts
  to useRoom()/useChat() with ZERO store subscriptions. They read
  actions directly via *Store.getState() in stable useCallbacks.

App
- CommandPalette: split into wrapper + content. The palette is always
  mounted by ChatLayout; the wrapper now returns null when closed so
  the content's hooks (useChat, useRoom, useRoster) only subscribe
  while visible.
- InviteToRoomModal: same wrapper/content split (also always mounted).
- RoomMembersModal, RoomHatsModal, JoinRoomModal, CreateQuickChatModal,
  OccupantPanel: swap useRoom() for useRoomActions().
- CreateRoomModal: useRoomActions() for actions + focused
  useAdminStore subscription for mucServiceJid.

Tests updated to mock useRoomActions where applicable.
MemberList and RoomsList are always mounted (right sidebar + main
sidebar), so over-subscription here re-renders unrelated UI on every
chat/room store update during background MAM sync.

- MemberList: use useChatActive() instead of useChat(). It only needed
  activeConversation; useChat() pulls in conversations list, typing
  states, drafts Map, MAM state, etc.

- RoomsList: replace useRoom() with focused useRoomStore selectors for
  allRooms / activeRoomJid plus useRoomActions() for the actions. Drop
  the list-level drafts Map subscription and push it down to a per-item
  useRoomStore subscription inside RoomItem (same pattern as
  ConversationItem) so a draft change in one room only re-renders that
  row, not the whole list.
@mremond mremond merged commit 35ecfe8 into main May 4, 2026
1 check passed
@mremond mremond deleted the fix/over-subscription-perf branch May 4, 2026 10:46
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