Skip to content

fix(ai-react): sync tools to ChatClient when tools prop changes#776

Open
citizen204 wants to merge 1 commit into
TanStack:mainfrom
citizen204:fix-775-use-chat-tools-update
Open

fix(ai-react): sync tools to ChatClient when tools prop changes#776
citizen204 wants to merge 1 commit into
TanStack:mainfrom
citizen204:fix-775-use-chat-tools-update

Conversation

@citizen204

@citizen204 citizen204 commented Jun 16, 2026

Copy link
Copy Markdown

Summary

useChat initialises the ChatClient with the tools option passed at construction time, but never calls client.updateOptions({ tools }) when the tools prop changes during the component lifecycle. This means the client-side tool registry becomes stale when callers swap tools — e.g. a project-management app where updateTaskTool closes over projectId and the user switches projects.

ChatClient.updateOptions() already accepts tools and rebuilds clientToolsRef when it receives them. The fix is to add tools to the existing updateOptions effect that already syncs body, forwardedProps, and context.

Changes

  • packages/ai-react/src/use-chat.ts: conditionally spread options.tools into the updateOptions call and add it to the dependency array, matching the existing pattern for forwardedProps.

Fixes #775

Test plan

  • Mount useChat with a tools array that closes over a value (e.g. projectId).
  • Change projectId while the component stays mounted.
  • Confirm the new tool implementation is used on the next LLM response (not the stale closure from mount time).
  • Callers should useMemo their tools array (as shown in the issue) to avoid unnecessary re-registrations.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of tool updates in chat functionality to ensure changes are properly reflected and take effect immediately.

@coderabbitai

coderabbitai Bot commented Jun 16, 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: 1b0a237f-c595-4e49-b933-373cdace273d

📥 Commits

Reviewing files that changed from the base of the PR and between eadabbc and ec6fd65.

📒 Files selected for processing (1)
  • packages/ai-react/src/use-chat.ts

📝 Walkthrough

Walkthrough

In useChat, the useEffect that calls client.updateOptions is modified to conditionally include tools: options.tools in the options object when options.tools is not undefined, and options.tools is added to the effect's dependency array so the update fires on tool changes.

Changes

Tools sync in useChat

Layer / File(s) Summary
Conditionally pass tools in updateOptions effect
packages/ai-react/src/use-chat.ts
The client.updateOptions call inside useEffect now spreads tools: options.tools when options.tools is not undefined, and options.tools is added to the dependency array to trigger re-execution on changes.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

  • TanStack/ai#752: Adds missing mountWithTools/notifyToolsChanged/recordStreamId methods to NoOpChatDevtoolsBridge, which is required for updateOptions({ tools }) to function correctly on first use.

Poem

A rabbit hops by with a bag full of tools,
"These closures grow stale — that breaks all the rules!"
Now updateOptions gets tools when they change,
No more stale projects left out of range.
🐇✨ The registry's fresh, and the chat is on track!

🚥 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 PR title accurately and concisely describes the main change: syncing tools to ChatClient when the tools prop changes.
Description check ✅ Passed The PR description provides clear context, explains the problem, describes the solution, and includes test guidance, but is missing the formal checklist items from the template.
Linked Issues check ✅ Passed The code changes directly address the requirement in issue #775 by adding tools to the updateOptions effect and dependency array, enabling ChatClient updates when tools prop changes.
Out of Scope Changes check ✅ Passed All changes in the PR are directly related to the stated objective of fixing tools synchronization in useChat, with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

Comment on lines 171 to 185
useEffect(() => {
// Conditional spread: `updateOptions` declares strict-optional
// fields and rejects explicit `undefined` under EOPT.
client.updateOptions({
body: options.body,
...(options.forwardedProps !== undefined && {
forwardedProps: options.forwardedProps,
}),
...(options.tools !== undefined && {
tools: options.tools,
}),
context: options.context,
})
}, [client, options.body, options.forwardedProps, options.context])
}, [client, options.body, options.forwardedProps, options.tools, options.context])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't it be better to have separate useEffect to avoid updating other fields repeatedly when one changes

the underlying code

just updates all fields present in the provided options.

With separate useEffects, updates in let's say forwardedRef wouldn't update the tools

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and show the updateOptions implementation in chat-client.ts
fd 'chat-client.ts' --type f | xargs grep -n 'updateOptions' | head -30
fd 'chat-client.ts' packages/ai-client --type f | xargs -I{} sed -n '1420,1480p' {}

Length of output: 2435


🏁 Script executed:

#!/bin/bash
# Get more of the updateOptions implementation, especially the tools handling
fd 'chat-client.ts' packages/ai-client --type f | xargs -I{} sed -n '1480,1540p' {}

Length of output: 1697



@manish-baghel Your concern is well-founded. Looking at the updateOptions implementation, the tools branch is not idempotent — it always rebuilds the tool registry and fires notifyToolsChanged():

if (options.tools !== undefined) {
  this.clientToolsRef.current = new Map()   // new Map every time
  for (const tool of options.tools) {
    this.clientToolsRef.current.set(tool.name, tool)
  }
  this.devtoolsBridge.notifyToolsChanged()  // devtools event every time
}

So with the current single combined effect, changing body or forwardedProps will trigger the effect, and if options.tools is defined it will unnecessarily rebuild the tool registry and fire a devtools notification. The same applies in reverse — changing tools re-sends body/forwardedProps/context even if those haven't changed (though those assignments are cheaper no-ops for body/forwardedProps since they only set a ref).

Separate effects are the cleaner fix:

useEffect(() => {
  client.updateOptions({ body: options.body })
}, [client, options.body])

useEffect(() => {
  if (options.forwardedProps !== undefined) {
    client.updateOptions({ forwardedProps: options.forwardedProps })
  }
}, [client, options.forwardedProps])

useEffect(() => {
  if (options.tools !== undefined) {
    client.updateOptions({ tools: options.tools })
  }
}, [client, options.tools])

useEffect(() => {
  client.updateOptions({ context: options.context })
}, [client, options.context])

This ensures each field update is surgical — a body change doesn't touch the tool registry, and a tools change doesn't re-send body or context. Would you like me to plan this refactor?

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.

@tanstack/ai-react: useChat does not update the underlying ChatClient when tools changes

2 participants