Skip to content
+

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:

  • messageIds stays reference-equal because the ID list did not change
  • Only the messagesById entry 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.

Selector-driven thread
selector-1 · renders 0

MUI Agent

Row 1 is subscribed independently.

selector-2 · renders 0

Alice

Row 2 is subscribed independently.

selector-3 · renders 0

MUI Agent

Row 3 is subscribed independently.

selector-4 · renders 0

Alice

Row 4 is subscribed independently.

selector-5 · renders 0

MUI Agent

Row 5 is subscribed independently.

selector-6 · renders 0

Alice

Row 6 is subscribed independently.

selector-7 · renders 0

MUI Agent

Row 7 is subscribed independently.

selector-8 · renders 0

Alice

Row 8 is subscribed independently.

selector-9 · renders 0

MUI Agent

Row 9 is subscribed independently.

selector-10 · renders 0

Alice

Row 10 is subscribed independently.

selector-11 · renders 0

MUI Agent

Row 11 is subscribed independently.

selector-12 · renders 0

Alice

Row 12 is subscribed independently.

selector-13 · renders 0

MUI Agent

Row 13 is subscribed independently.

selector-14 · renders 0

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() and useConversation(id) follow the same pattern for conversation lists

See also

API