refactor(ai-react): construct useChat ChatClient via useState lazy init#864
refactor(ai-react): construct useChat ChatClient via useState lazy init#864jingeon27 wants to merge 1 commit into
Conversation
`useMemo` is a performance hint React may discard and recompute, which could spuriously construct a second ChatClient (each owns a StreamProcessor, a devtools bridge, and a connection). Switch to a `useState` lazy initializer — a per-mount "runs once" guarantee — recreating the client synchronously on `id` change via the adjust-state-during-render pattern. Also removes an unused `isFirstMountRef`. No public API or behavior change; the 152 useChat unit tests are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
useChat lazy ChatClient construction
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
useChatconstructs its internalChatClientwithuseMemo(() => …, [clientId]). React's docs are explicit thatuseMemois a performance hint, not a semantic guarantee — React may discard a memoized value and recompute it. For an instance you need to keep stable, that's the wrong contract: a spurious recompute constructs a secondChatClient, and each one owns aStreamProcessor, a devtools bridge, and a connection.This switches client construction to a
useStatelazy initializer — the documented "runs once per mount" guarantee, which is what React recommends for creating instances. The client is still recreated synchronously whenidchanges:useStatehas no dependency array, so this uses the documented "adjust state during render" pattern (track the previousid, swap during render when it differs). The existing[client]effect still disposes the superseded instance, so lifecycle is unchanged.While here, also removes an unused
isFirstMountRef.Changes
packages/ai-react/src/use-chat.ts:const client = useMemo(() => {…}, [clientId])with acreateClient()factory +useState(createClient)lazy initializer, recreating onidchange via the previous-clientId-in-state pattern.isFirstMountRef.messagesRefrender-sync comment (it referenceduseMemo).Test plan
@tanstack/ai-reactunit suite: 152/152 pass, unchanged — including theclient recreationtests, notably "return new client messages during the id change render", which asserts the swap stays synchronous.tsc,eslint,build, andpublintall green.No public API change and no observable behavior change — the instance is still created during render (same timing as
useMemo); this only upgrades the memoization primitive to the one with a runtime guarantee.🤖 Generated with Claude Code
Summary by CodeRabbit