Skip to content

Commit 550e413

Browse files
authored
fix(react): preserve chat when id is undefined (#16484)
## Background Passing `id: undefined` to `useChat` currently recreates the internal `Chat` instance on every render because the `id` key exists in options while the auto-generated chat ID never equals `undefined`. That clears messages and stream state immediately after updates, which makes the common `id={conversationId ?? undefined}` pattern behave differently from omitting `id`. I confirmed the reproduction from #16226 with a focused regression test: with the old guard, calling `setMessages` while `id` is explicitly `undefined` immediately rerenders into a fresh chat with a new generated ID and `messages: []`. I compared the two open PRs before choosing this implementation: - #16228 by @Aayush-engineer uses the nullish guard and adds a changeset, but does not include a regression test. - #16403 by @zhsj0089944 uses an `undefined` guard, but does not include a regression test or changeset. This PR uses the nullish guard from #16228 because it makes explicit `undefined` behave like an omitted ID and also treats a JavaScript `null` value defensively as “no provided ID”. It adds the missing hook-level regression test and a patch changeset. Credit to @Aayush-engineer and @zhsj0089944 for the prior PRs and diagnosis. ## Summary - Only recreate the internal `Chat` from `useChat` when a provided `id` is non-nullish and different from the current chat ID. - Add a regression test that verifies `useChat({ id: undefined })` preserves the generated ID and messages across rerenders. - Add a patch changeset for `@ai-sdk/react`. ## Manual Verification Confirmed the reproduction by temporarily restoring the old guard and running: ```sh pnpm --filter @ai-sdk/react exec vitest --config vitest.config.js --run src/use-chat.ui.test.tsx -t "should not recreate chat when id is explicitly undefined" ``` With the old guard, the test failed because messages reset to `[]` and the generated chat ID changed. With this fix, the same test passes. ## Checklist - [x] All commits are signed (PRs with unsigned commits cannot be merged) - [x] Tests have been added / updated (for bug fixes / features) - [ ] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] I have reviewed this pull request (self-review) ## Related Issues Fixes #16226. Supersedes and closes #16228. Supersedes and closes #16403.
1 parent b19977f commit 550e413

3 files changed

Lines changed: 86 additions & 1 deletion

File tree

.changeset/use-chat-nullish-id.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ai-sdk/react": patch
3+
---
4+
5+
Treat nullish `useChat` IDs the same as omitted IDs so the chat instance is not recreated on every render.

packages/react/src/use-chat.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ export function useChat<UI_MESSAGE extends UIMessage = UIMessage>({
9696

9797
const shouldRecreateChat =
9898
('chat' in options && options.chat !== chatRef.current) ||
99-
('id' in options && chatRef.current.id !== options.id);
99+
('id' in options &&
100+
options.id != null &&
101+
chatRef.current.id !== options.id);
100102

101103
if (shouldRecreateChat) {
102104
chatRef.current =

packages/react/src/use-chat.ui.test.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,84 @@ describe('use-chat', () => {
24242424
});
24252425
});
24262426

2427+
describe('undefined id', () => {
2428+
setupTestComponent(
2429+
() => {
2430+
const [renderCount, setRenderCount] = React.useState(0);
2431+
const {
2432+
messages,
2433+
setMessages,
2434+
id: idKey,
2435+
} = useChat({
2436+
id: undefined,
2437+
generateId: mockId(),
2438+
});
2439+
2440+
return (
2441+
<div>
2442+
<div data-testid="id">{idKey}</div>
2443+
<div data-testid="render-count">{renderCount}</div>
2444+
<div data-testid="messages">
2445+
{JSON.stringify(messages, null, 2)}
2446+
</div>
2447+
<button
2448+
data-testid="set-message"
2449+
onClick={() => {
2450+
setMessages([
2451+
{
2452+
id: 'message-0',
2453+
role: 'user',
2454+
parts: [{ text: 'hi', type: 'text' }],
2455+
},
2456+
]);
2457+
}}
2458+
/>
2459+
<button
2460+
data-testid="rerender"
2461+
onClick={() => {
2462+
setRenderCount(count => count + 1);
2463+
}}
2464+
/>
2465+
</div>
2466+
);
2467+
},
2468+
{
2469+
init: TestComponent => <TestComponent />,
2470+
},
2471+
);
2472+
2473+
it('should not recreate chat when id is explicitly undefined', async () => {
2474+
const initialId = screen.getByTestId('id').textContent;
2475+
2476+
await userEvent.click(screen.getByTestId('set-message'));
2477+
2478+
await waitFor(() => {
2479+
expect(
2480+
JSON.parse(screen.getByTestId('messages').textContent ?? ''),
2481+
).toStrictEqual([
2482+
{
2483+
id: 'message-0',
2484+
role: 'user',
2485+
parts: [{ text: 'hi', type: 'text' }],
2486+
},
2487+
]);
2488+
});
2489+
2490+
await userEvent.click(screen.getByTestId('rerender'));
2491+
2492+
expect(screen.getByTestId('id').textContent).toBe(initialId);
2493+
expect(
2494+
JSON.parse(screen.getByTestId('messages').textContent ?? ''),
2495+
).toStrictEqual([
2496+
{
2497+
id: 'message-0',
2498+
role: 'user',
2499+
parts: [{ text: 'hi', type: 'text' }],
2500+
},
2501+
]);
2502+
});
2503+
});
2504+
24272505
describe('chat instance changes', () => {
24282506
setupTestComponent(
24292507
() => {

0 commit comments

Comments
 (0)