Chat - Selector-driven thread
Render large custom threads efficiently with IDs at the list level and row-level message subscriptions.
This demo demonstrates the performance-focused rendering pattern for threads with many messages. Instead of subscribing the entire list to every message change, each row subscribes only to its own message record.
Key concepts
The ID list + row subscription pattern
The parent component calls useMessageIds() to get the ordered list of message IDs.
Each row component calls useMessage(id) to subscribe to its own message:
function Thread() {
const messageIds = useMessageIds();
return (
<div>
{messageIds.map((id) => (
<MessageRow key={id} id={id} />
))}
</div>
);
}
const MessageRow = React.memo(function MessageRow({ id }: { id: string }) {
const message = useMessage(id);
if (!message) return null;
return (
<div>{message.parts[0]?.type === 'text' ? message.parts[0].text : null}</div>
);
});
Why this matters
The store keeps messages in a normalized shape (messageIds + messagesById).
When a single message updates during streaming:
messageIdsstays reference-equal because the ID list did not change- Only the
messagesByIdentry for the updated message changes useMessage(id)on the updated row triggers a re-render- All other rows stay untouched
This means that for a thread with 100 messages where one is streaming, only one component re-renders per delta — not 100.
Conversation-level selectors
The same pattern applies to conversations:
const conversations = useConversations();
const conversation = useConversation('selectors');
Selector-driven thread
Update one controlled message from the parent to see only the matching row rerender.
MUI Agent
Row 1 is subscribed independently.
Alice
Row 2 is subscribed independently.
MUI Agent
Row 3 is subscribed independently.
Alice
Row 4 is subscribed independently.
MUI Agent
Row 5 is subscribed independently.
Alice
Row 6 is subscribed independently.
MUI Agent
Row 7 is subscribed independently.
Alice
Row 8 is subscribed independently.
MUI Agent
Row 9 is subscribed independently.
Alice
Row 10 is subscribed independently.
MUI Agent
Row 11 is subscribed independently.
Alice
Row 12 is subscribed independently.
MUI Agent
Row 13 is subscribed independently.
Alice
Row 14 is subscribed independently.
Key takeaways
useMessageIds()+useMessage(id)is the recommended pattern for threads with more than a handful of messages- The normalized store ensures stable references — only changed data triggers re-renders
- Wrap row components in
React.memo()for maximum efficiency useConversations()anduseConversation(id)follow the same pattern for conversation lists
See also
- Selectors for the full selector API and custom subscriptions
- Hooks for all available hooks
- Advanced store access for custom selectors with
useChatStore()