Skip to content
+

Chat - Selector-driven thread

Render large custom threads efficiently by subscribing rows to individual message IDs.

Use this pattern to render large custom threads efficiently when subscribing the entire list to every message change would be too costly. Instead of subscribing the entire list to every message change, each row subscribes only to its own message record.

Key concepts

ID list and 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.

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, as shown below:

const conversations = useConversations();
const conversation = useConversation('selectors');

The demo below shows the selector-driven thread pattern end to end:

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

  • Selectors for details on the full selector API and custom subscriptions.
  • Hooks for details on all available hooks.
  • Advanced store access for details on custom selectors with useChatStore().

API