Skip to content

refactor(ai-react): construct useChat ChatClient via useState lazy init#864

Open
jingeon27 wants to merge 1 commit into
TanStack:mainfrom
jingeon27:refactor/usechat-lazy-client-instance
Open

refactor(ai-react): construct useChat ChatClient via useState lazy init#864
jingeon27 wants to merge 1 commit into
TanStack:mainfrom
jingeon27:refactor/usechat-lazy-client-instance

Conversation

@jingeon27

@jingeon27 jingeon27 commented Jun 30, 2026

Copy link
Copy Markdown

Summary

useChat constructs its internal ChatClient with useMemo(() => …, [clientId]). React's docs are explicit that useMemo is 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 second ChatClient, and each one owns a StreamProcessor, a devtools bridge, and a connection.

This switches client construction to a useState lazy initializer — the documented "runs once per mount" guarantee, which is what React recommends for creating instances. The client is still recreated synchronously when id changes: useState has no dependency array, so this uses the documented "adjust state during render" pattern (track the previous id, 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:
    • Replace const client = useMemo(() => {…}, [clientId]) with a createClient() factory + useState(createClient) lazy initializer, recreating on id change via the previous-clientId-in-state pattern.
    • Remove the unused isFirstMountRef.
    • Update the adjacent messagesRef render-sync comment (it referenced useMemo).

Test plan

  • @tanstack/ai-react unit suite: 152/152 pass, unchanged — including the client recreation tests, notably "return new client messages during the id change render", which asserts the swap stays synchronous.
  • tsc, eslint, build, and publint all 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

  • Bug Fixes
    • Improved chat hook stability by making its internal client creation more predictable during React renders.
    • When the chat identifier changes, the client is now refreshed immediately and consistently.
    • Removed an unnecessary internal dependency, reducing the chance of extra recomputation.

`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>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a2cf0b0f-5fb9-4ea5-8760-658f133d7cf1

📥 Commits

Reviewing files that changed from the base of the PR and between c9b7f98 and d5cf35b.

📒 Files selected for processing (2)
  • .changeset/usechat-lazy-client-instance.md
  • packages/ai-react/src/use-chat.ts

📝 Walkthrough

Walkthrough

useChat switches from useMemo to a render-time createClient() function with useState for constructing ChatClient. A new trackedClientId state replaces the clientId-dependency memoization path, triggering an immediate client swap during render when clientId changes. A changeset documents the patch.

useChat lazy ChatClient construction

Layer / File(s) Summary
createClient() and useState-based client initialization
packages/ai-react/src/use-chat.ts
Introduces createClient() and activeClientRef/cleanupInvalidationRef refs, replacing useMemo-based ChatClient construction. Adds trackedClientId state so that when clientId changes, setClient(createClient()) is called during render to swap the instance immediately.
Changeset
.changeset/usechat-lazy-client-instance.md
Documents the patch to @tanstack/ai-react covering the useMemo-to-useState change and unused ref removal.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • TanStack/ai#661: Overlaps on the clientId-based ChatClient recreation path in useChat, adding persistence hydration keyed by id and ensuring id-switch behavior is consistent.

Suggested reviewers

  • tombeckenham

Poem

🐇 No more useMemo tricks,
A lazy useState now does the fix!
When clientId shifts mid-render's flight,
createClient() swaps the instance right.
Hop hop, the ref is gone, no fuss —
React won't recompute without a fuss! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change to useChat client construction.
Description check ✅ Passed The description covers the summary, changes, test plan, and release impact, matching the template’s main required information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant