@tanstack/ai-preact 0.8.2 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,17 +17,21 @@ function useChat(options) {
17
17
  options.initialMessages || []
18
18
  );
19
19
  const isFirstMountRef = useRef(true);
20
+ const activeClientRef = useRef(null);
21
+ const cleanupInvalidationRef = useRef(
22
+ null
23
+ );
20
24
  const optionsRef = useRef(options);
21
25
  optionsRef.current = options;
22
26
  useEffect(() => {
23
27
  messagesRef.current = messages;
24
28
  }, [messages]);
25
29
  const client = useMemo(() => {
26
- const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
30
+ const messagesToUse = options.initialMessages || [];
27
31
  isFirstMountRef.current = false;
28
32
  const initialOptions = optionsRef.current;
29
33
  const transport = initialOptions.connection ? { connection: initialOptions.connection } : { fetcher: initialOptions.fetcher };
30
- return new ChatClient({
34
+ const instance = new ChatClient({
31
35
  devtoolsBridgeFactory: createChatDevtoolsBridge,
32
36
  ...transport,
33
37
  id: clientId,
@@ -36,6 +40,9 @@ function useChat(options) {
36
40
  ...initialOptions.forwardedProps !== void 0 && {
37
41
  forwardedProps: initialOptions.forwardedProps
38
42
  },
43
+ ...initialOptions.persistence !== void 0 && {
44
+ persistence: initialOptions.persistence
45
+ },
39
46
  ...initialOptions.context !== void 0 && {
40
47
  context: initialOptions.context
41
48
  },
@@ -48,15 +55,26 @@ function useChat(options) {
48
55
  // Wrap every callback so the latest options are read at call time.
49
56
  // Capturing the function reference directly would freeze it to whatever
50
57
  // the parent passed on the first render.
51
- onResponse: (response) => optionsRef.current.onResponse?.(response),
52
- onChunk: (chunk) => optionsRef.current.onChunk?.(chunk),
58
+ onResponse: (response) => {
59
+ if (activeClientRef.current !== instance) return;
60
+ return optionsRef.current.onResponse?.(response);
61
+ },
62
+ onChunk: (chunk) => {
63
+ if (activeClientRef.current !== instance) return;
64
+ optionsRef.current.onChunk?.(chunk);
65
+ },
53
66
  onFinish: (message) => {
67
+ if (activeClientRef.current !== instance) return;
54
68
  optionsRef.current.onFinish?.(message);
55
69
  },
56
70
  onError: (err) => {
71
+ if (activeClientRef.current !== instance) return;
57
72
  optionsRef.current.onError?.(err);
58
73
  },
59
- onCustomEvent: (eventType, data, context) => optionsRef.current.onCustomEvent?.(eventType, data, context),
74
+ onCustomEvent: (eventType, data, context) => {
75
+ if (activeClientRef.current !== instance) return;
76
+ optionsRef.current.onCustomEvent?.(eventType, data, context);
77
+ },
60
78
  ...initialOptions.tools !== void 0 && {
61
79
  tools: initialOptions.tools
62
80
  },
@@ -64,28 +82,43 @@ function useChat(options) {
64
82
  streamProcessor: options.streamProcessor
65
83
  },
66
84
  onMessagesChange: (newMessages) => {
85
+ if (activeClientRef.current !== instance) return;
67
86
  setMessages(newMessages);
68
87
  },
69
88
  onLoadingChange: (newIsLoading) => {
89
+ if (activeClientRef.current !== instance) return;
70
90
  setIsLoading(newIsLoading);
71
91
  },
72
92
  onStatusChange: (newStatus) => {
93
+ if (activeClientRef.current !== instance) return;
73
94
  setStatus(newStatus);
74
95
  },
75
96
  onErrorChange: (newError) => {
97
+ if (activeClientRef.current !== instance) return;
76
98
  setError(newError);
77
99
  },
78
100
  onSubscriptionChange: (nextIsSubscribed) => {
101
+ if (activeClientRef.current !== instance) return;
79
102
  setIsSubscribed(nextIsSubscribed);
80
103
  },
81
104
  onConnectionStatusChange: (nextStatus) => {
105
+ if (activeClientRef.current !== instance) return;
82
106
  setConnectionStatus(nextStatus);
83
107
  },
84
108
  onSessionGeneratingChange: (isGenerating) => {
109
+ if (activeClientRef.current !== instance) return;
85
110
  setSessionGenerating(isGenerating);
86
111
  }
87
112
  });
113
+ activeClientRef.current = instance;
114
+ return instance;
88
115
  }, [clientId]);
116
+ useEffect(() => {
117
+ const clientMessages = client.getMessages();
118
+ if (clientMessages !== messagesRef.current) {
119
+ setMessages(clientMessages);
120
+ }
121
+ }, [client]);
89
122
  useEffect(() => {
90
123
  client.updateOptions({
91
124
  body: options.body,
@@ -95,11 +128,6 @@ function useChat(options) {
95
128
  context: options.context
96
129
  });
97
130
  }, [client, options.body, options.forwardedProps, options.context]);
98
- useEffect(() => {
99
- if (options.initialMessages && options.initialMessages.length && !messages.length) {
100
- client.setMessagesManually(options.initialMessages);
101
- }
102
- }, []);
103
131
  useEffect(() => {
104
132
  if (options.live) {
105
133
  client.subscribe();
@@ -108,8 +136,19 @@ function useChat(options) {
108
136
  }
109
137
  }, [client, options.live]);
110
138
  useEffect(() => {
139
+ if (cleanupInvalidationRef.current) {
140
+ clearTimeout(cleanupInvalidationRef.current);
141
+ cleanupInvalidationRef.current = null;
142
+ }
143
+ activeClientRef.current = client;
111
144
  client.mountDevtools();
112
145
  return () => {
146
+ cleanupInvalidationRef.current = setTimeout(() => {
147
+ if (activeClientRef.current === client) {
148
+ activeClientRef.current = null;
149
+ }
150
+ cleanupInvalidationRef.current = null;
151
+ }, 0);
113
152
  if (optionsRef.current.live) {
114
153
  client.unsubscribe();
115
154
  } else {
@@ -157,8 +196,9 @@ function useChat(options) {
157
196
  },
158
197
  [client]
159
198
  );
199
+ const renderedMessages = client.getMessages();
160
200
  return {
161
- messages,
201
+ messages: renderedMessages,
162
202
  sendMessage,
163
203
  append,
164
204
  reload,
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n} from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\n\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n>(options: UseChatOptions<TTools, TContext>): UseChatReturn<TTools> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)\n\n optionsRef.current = options\n\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n const client = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n return new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => optionsRef.current.onResponse?.(response),\n onChunk: (chunk) => optionsRef.current.onChunk?.(chunk),\n onFinish: (message) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n optionsRef.current.onError?.(err)\n },\n onCustomEvent: (eventType, data, context) =>\n optionsRef.current.onCustomEvent?.(eventType, data, context),\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n })\n }, [clientId])\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure Preact state is in sync\n useEffect(() => {\n if (\n options.initialMessages &&\n options.initialMessages.length &&\n !messages.length\n ) {\n client.setMessagesManually(options.initialMessages)\n }\n }, [])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n client.mountDevtools()\n\n return () => {\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the client.\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: unknown\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n} from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\n\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n>(options: UseChatOptions<TTools, TContext>): UseChatReturn<TTools> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n const [status, setStatus] = useState<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n const activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)\n\n optionsRef.current = options\n\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(err)\n },\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onStatusChange: (newStatus: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\n\n // Cleanup on unmount: stop any in-flight requests\n // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating the client.\n const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n async (result: {\n toolCallId: string\n tool: string\n output: unknown\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client.addToolResult(result)\n },\n [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\n )\n\n const renderedMessages = client.getMessages()\n\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-preact",
3
- "version": "0.8.2",
3
+ "version": "0.9.2",
4
4
  "description": "Preact hooks for TanStack AI streaming chat and typed messages.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "tool-calling"
35
35
  ],
36
36
  "dependencies": {
37
- "@tanstack/ai-client": "0.15.2"
37
+ "@tanstack/ai-client": "0.16.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@testing-library/preact": "^3.2.4",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "preact": ">=10.11.0",
48
- "@tanstack/ai": "^0.26.0"
48
+ "@tanstack/ai": "^0.27.0"
49
49
  },
50
50
  "scripts": {
51
51
  "clean": "premove ./build ./dist",
package/src/use-chat.ts CHANGED
@@ -45,6 +45,10 @@ export function useChat<
45
45
  options.initialMessages || [],
46
46
  )
47
47
  const isFirstMountRef = useRef(true)
48
+ const activeClientRef = useRef<ChatClient | null>(null)
49
+ const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(
50
+ null,
51
+ )
48
52
  const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)
49
53
 
50
54
  optionsRef.current = options
@@ -54,11 +58,7 @@ export function useChat<
54
58
  }, [messages])
55
59
 
56
60
  const client = useMemo(() => {
57
- // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.
58
- const messagesToUse = isFirstMountRef.current
59
- ? options.initialMessages || []
60
- : messagesRef.current
61
-
61
+ const messagesToUse = options.initialMessages || []
62
62
  isFirstMountRef.current = false
63
63
 
64
64
  // Build options with conditional spreads for fields whose source
@@ -70,7 +70,7 @@ export function useChat<
70
70
  ? { connection: initialOptions.connection }
71
71
  : { fetcher: initialOptions.fetcher }
72
72
 
73
- return new ChatClient<TTools, TContext>({
73
+ const instance = new ChatClient<TTools, TContext>({
74
74
  devtoolsBridgeFactory: createChatDevtoolsBridge,
75
75
  ...transport,
76
76
  id: clientId,
@@ -79,6 +79,9 @@ export function useChat<
79
79
  ...(initialOptions.forwardedProps !== undefined && {
80
80
  forwardedProps: initialOptions.forwardedProps,
81
81
  }),
82
+ ...(initialOptions.persistence !== undefined && {
83
+ persistence: initialOptions.persistence,
84
+ }),
82
85
  ...(initialOptions.context !== undefined && {
83
86
  context: initialOptions.context,
84
87
  }),
@@ -91,16 +94,26 @@ export function useChat<
91
94
  // Wrap every callback so the latest options are read at call time.
92
95
  // Capturing the function reference directly would freeze it to whatever
93
96
  // the parent passed on the first render.
94
- onResponse: (response) => optionsRef.current.onResponse?.(response),
95
- onChunk: (chunk) => optionsRef.current.onChunk?.(chunk),
97
+ onResponse: (response) => {
98
+ if (activeClientRef.current !== instance) return
99
+ return optionsRef.current.onResponse?.(response)
100
+ },
101
+ onChunk: (chunk) => {
102
+ if (activeClientRef.current !== instance) return
103
+ optionsRef.current.onChunk?.(chunk)
104
+ },
96
105
  onFinish: (message) => {
106
+ if (activeClientRef.current !== instance) return
97
107
  optionsRef.current.onFinish?.(message)
98
108
  },
99
109
  onError: (err) => {
110
+ if (activeClientRef.current !== instance) return
100
111
  optionsRef.current.onError?.(err)
101
112
  },
102
- onCustomEvent: (eventType, data, context) =>
103
- optionsRef.current.onCustomEvent?.(eventType, data, context),
113
+ onCustomEvent: (eventType, data, context) => {
114
+ if (activeClientRef.current !== instance) return
115
+ optionsRef.current.onCustomEvent?.(eventType, data, context)
116
+ },
104
117
  ...(initialOptions.tools !== undefined && {
105
118
  tools: initialOptions.tools,
106
119
  }),
@@ -108,29 +121,45 @@ export function useChat<
108
121
  streamProcessor: options.streamProcessor,
109
122
  }),
110
123
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
124
+ if (activeClientRef.current !== instance) return
111
125
  setMessages(newMessages)
112
126
  },
113
127
  onLoadingChange: (newIsLoading: boolean) => {
128
+ if (activeClientRef.current !== instance) return
114
129
  setIsLoading(newIsLoading)
115
130
  },
116
131
  onStatusChange: (newStatus: ChatClientState) => {
132
+ if (activeClientRef.current !== instance) return
117
133
  setStatus(newStatus)
118
134
  },
119
135
  onErrorChange: (newError: Error | undefined) => {
136
+ if (activeClientRef.current !== instance) return
120
137
  setError(newError)
121
138
  },
122
139
  onSubscriptionChange: (nextIsSubscribed: boolean) => {
140
+ if (activeClientRef.current !== instance) return
123
141
  setIsSubscribed(nextIsSubscribed)
124
142
  },
125
143
  onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
144
+ if (activeClientRef.current !== instance) return
126
145
  setConnectionStatus(nextStatus)
127
146
  },
128
147
  onSessionGeneratingChange: (isGenerating: boolean) => {
148
+ if (activeClientRef.current !== instance) return
129
149
  setSessionGenerating(isGenerating)
130
150
  },
131
151
  })
152
+ activeClientRef.current = instance
153
+ return instance
132
154
  }, [clientId])
133
155
 
156
+ useEffect(() => {
157
+ const clientMessages = client.getMessages()
158
+ if (clientMessages !== messagesRef.current) {
159
+ setMessages(clientMessages)
160
+ }
161
+ }, [client])
162
+
134
163
  // Sync body / forwardedProps changes to the client.
135
164
  // Both populate the same wire payload; `forwardedProps` is preferred
136
165
  // and `body` is deprecated but still supported.
@@ -146,19 +175,6 @@ export function useChat<
146
175
  })
147
176
  }, [client, options.body, options.forwardedProps, options.context])
148
177
 
149
- // Sync initial messages on mount only
150
- // Note: initialMessages are passed to ChatClient constructor, but we also
151
- // set them here to ensure Preact state is in sync
152
- useEffect(() => {
153
- if (
154
- options.initialMessages &&
155
- options.initialMessages.length &&
156
- !messages.length
157
- ) {
158
- client.setMessagesManually(options.initialMessages)
159
- }
160
- }, [])
161
-
162
178
  useEffect(() => {
163
179
  if (options.live) {
164
180
  client.subscribe()
@@ -172,9 +188,20 @@ export function useChat<
172
188
  // DO NOT include isLoading in dependencies - that would cause the cleanup
173
189
  // to run when isLoading changes, aborting continuation requests.
174
190
  useEffect(() => {
191
+ if (cleanupInvalidationRef.current) {
192
+ clearTimeout(cleanupInvalidationRef.current)
193
+ cleanupInvalidationRef.current = null
194
+ }
195
+ activeClientRef.current = client
175
196
  client.mountDevtools()
176
197
 
177
198
  return () => {
199
+ cleanupInvalidationRef.current = setTimeout(() => {
200
+ if (activeClientRef.current === client) {
201
+ activeClientRef.current = null
202
+ }
203
+ cleanupInvalidationRef.current = null
204
+ }, 0)
178
205
  // Subscribe/unsubscribe on `options.live` is owned by the dedicated
179
206
  // effect above. This cleanup only fires on unmount or client swap,
180
207
  // so read `live` through the ref to avoid disposing the client every
@@ -243,8 +270,10 @@ export function useChat<
243
270
  [client],
244
271
  )
245
272
 
273
+ const renderedMessages = client.getMessages()
274
+
246
275
  return {
247
- messages,
276
+ messages: renderedMessages,
248
277
  sendMessage,
249
278
  append,
250
279
  reload,