Skip to content

useObjectChat rebuilds its DefaultChatTransport on every render — the memo's body dep is an inline literal every caller recreates (finding, dormant) #4187

Description

@yinlianghui

Observation-class. Nothing a user hits today; filing so it is on the record rather than rediscovered.

packages/plugin-chatbot/src/useObjectChat.ts memoizes the transport:

consttransport=useMemo(()=>{if(!isApiMode)returnundefined;returnnewDefaultChatTransport({api: api!, ... });},[isApiMode,api,headers,body,model,systemPrompt,streamingEnabled,conversationId]);

body and headers are object props, and every caller passes a fresh literal on each render. AiChatPage's ChatPane is the clearest case:

body: {context: {activeApp: 'AI',agentName: activeAgent,autoPublishAiBuilds: getRuntimeConfig().features.autoPublishAiBuilds,
...(editPackageId ? {packageId: editPackageId} : {}),},},

That object has a new identity on every render, so the memo never hits and a DefaultChatTransport is constructed on every render of every chat surface — during a streaming turn, that is once per token batch.

Why it is dormant rather than broken

@ai-sdk/react@4.0.59's useChat does not rebuild its Chat when the transport identity changes. It stores the latest transport in a ref and its own chatOptions.transport delegates through getTransport():

latestRef.current={ ...,transport: options.transport};constchatOptions={ ...,transport: {sendMessages: (o)=>getTransport().sendMessages(o), ... }};constshouldRecreateChat="chat"inoptions&&options.chat!==chatRef.current||"id"inoptions&&options.id!=null&&chatRef.current.id!==options.id;

useObjectChat passes neither chat nor id, so the Chat object — and with it the whole message list — survives. The wasted construction is the entire cost.

Why it is worth recording anyway

The safety here is entirely a property of the SDK's internals, not of this call site. The memo looks like it is protecting something, and the dep list reads as if a changing transport would be disruptive. An SDK upgrade that starts keying the Chat on the transport (or a future id being threaded through) would turn a silent inefficiency into a thread that resets on every render — and the memo would give a reviewer no warning, because it is already written as though it works.

Two candidate directions, neither obviously right without measuring:

  • memoize at the call sites (body / headers behind useMemo), which fixes it wherever it is done but is unenforceable and will re-rot;
  • drop body/headers from the dep list and read them through a ref inside prepareSendMessagesRequest, which is what the file already does for model (modelRef) and for the handoff id (parentConvRef) — the established idiom in this very hook, and the only one that cannot be undone by a new caller.

Found while working #2627 (the AI build thread's blank-out was a different mechanism entirely — a pane remount driven by useChatConversation clearing the conversation id; PR #4183). This was ruled out as its cause and is unrelated to it.

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions