From 9268801afd3262b17778130d4d3577eb79f54c9c Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Wed, 19 Aug 2026 10:31:02 +0800 Subject: [PATCH 1/3] fix(ai-react): avoid random IDs during SSR render --- packages/ai-client/src/devtools.ts | 20 ++++---------------- packages/ai-react/src/use-chat.ts | 1 + 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/ai-client/src/devtools.ts b/packages/ai-client/src/devtools.ts index 11fc90fe9..7e16342c8 100644 --- a/packages/ai-client/src/devtools.ts +++ b/packages/ai-client/src/devtools.ts @@ -742,23 +742,11 @@ export class ClientDevtoolsBridge { } } -let bridgeIdSequence = 0 - function createBridgeId(hookId: string): string { - const cryptoLike = ( - globalThis as { - crypto?: { - randomUUID?: () => string - } - } - ).crypto - - if (cryptoLike?.randomUUID) { - return `bridge:${hookId}:${cryptoLike.randomUUID()}` - } - - bridgeIdSequence += 1 - return `bridge:${hookId}:${bridgeIdSequence}` + // hookId comes from React's useId and is stable across SSR and hydration. + // Do not generate randomness during render: crypto.randomUUID is unavailable + // or restricted in some server runtimes, and discarded renders must be pure. + return `bridge:${hookId}` } // Owns the chat-client devtools surface so the chat client itself stays a diff --git a/packages/ai-react/src/use-chat.ts b/packages/ai-react/src/use-chat.ts index 96a75eb9c..70e19cd44 100644 --- a/packages/ai-react/src/use-chat.ts +++ b/packages/ai-react/src/use-chat.ts @@ -131,6 +131,7 @@ export function useChat< ...(initialOptions.threadId !== undefined && { threadId: initialOptions.threadId, }), + ...(initialOptions.threadId === undefined && { threadId: hookId }), ...(initialOptions.forwardedProps !== undefined && { forwardedProps: initialOptions.forwardedProps, }), From 8b182798d755bea16688f279b950b157b219fc6d Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Wed, 19 Aug 2026 17:59:27 +0800 Subject: [PATCH 2/3] fix(ai-react): normalize empty thread ids --- packages/ai-react/src/use-chat.ts | 11 ++++------- packages/ai-react/tests/use-chat.test.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/ai-react/src/use-chat.ts b/packages/ai-react/src/use-chat.ts index 70e19cd44..f637a4934 100644 --- a/packages/ai-react/src/use-chat.ts +++ b/packages/ai-react/src/use-chat.ts @@ -41,10 +41,10 @@ export function useChat< ): UseChatReturn { // The hook's identity is its `threadId` — also the persistence key, so a // reload with the same `threadId` restores the same conversation. `hookId` is - // only a stable fallback for React's client-recreation keying when no - // `threadId` is given (an ephemeral chat), never a persistence key. + // a stable fallback when an empty or missing `threadId` creates an ephemeral + // chat, never a persistence key. const hookId = useId() - const clientId = options.threadId ?? hookId + const clientId = options.threadId || hookId const [messages, setMessages] = useState>>( options.initialMessages || [], @@ -128,10 +128,7 @@ export function useChat< ...transport, initialMessages: messagesToUse, ...(initialOptions.body !== undefined && { body: initialOptions.body }), - ...(initialOptions.threadId !== undefined && { - threadId: initialOptions.threadId, - }), - ...(initialOptions.threadId === undefined && { threadId: hookId }), + threadId: clientId, ...(initialOptions.forwardedProps !== undefined && { forwardedProps: initialOptions.forwardedProps, }), diff --git a/packages/ai-react/tests/use-chat.test.ts b/packages/ai-react/tests/use-chat.test.ts index d172ab858..3c6452f52 100644 --- a/packages/ai-react/tests/use-chat.test.ts +++ b/packages/ai-react/tests/use-chat.test.ts @@ -290,6 +290,20 @@ describe('useChat', () => { expect(typeof messageId).toBe('string') }) + it('uses the hook ID instead of generating a random ID for an empty threadId', () => { + const chatClientPrototype = ChatClient.prototype as unknown as { + generateUniqueId: () => string + } + const generateUniqueId = vi.spyOn(chatClientPrototype, 'generateUniqueId') + + renderUseChat({ + connection: createMockConnectionAdapter(), + threadId: '', + }) + + expect(generateUniqueId).not.toHaveBeenCalled() + }) + it('should generate id if not provided', async () => { const chunks = createTextChunks('Response') const adapter = createMockConnectionAdapter({ chunks }) From cc686fa7712e119bdff59f656cd20e4ee54fb4e8 Mon Sep 17 00:00:00 2001 From: mikemikimike <13286568797@163.com> Date: Thu, 20 Aug 2026 00:25:43 +0800 Subject: [PATCH 3/3] test(ai-react): cover omitted thread ids --- packages/ai-react/tests/use-chat.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ai-react/tests/use-chat.test.ts b/packages/ai-react/tests/use-chat.test.ts index 3c6452f52..3edfa515c 100644 --- a/packages/ai-react/tests/use-chat.test.ts +++ b/packages/ai-react/tests/use-chat.test.ts @@ -290,7 +290,9 @@ describe('useChat', () => { expect(typeof messageId).toBe('string') }) - it('uses the hook ID instead of generating a random ID for an empty threadId', () => { + it.each(['', undefined])( + 'uses the hook ID instead of generating a random ID for threadId=%s', + (threadId) => { const chatClientPrototype = ChatClient.prototype as unknown as { generateUniqueId: () => string } @@ -298,10 +300,11 @@ describe('useChat', () => { renderUseChat({ connection: createMockConnectionAdapter(), - threadId: '', + ...(threadId === undefined ? {} : { threadId }), }) - expect(generateUniqueId).not.toHaveBeenCalled() + expect(generateUniqueId).not.toHaveBeenCalled() + }, }) it('should generate id if not provided', async () => {