@tanstack/ai-preact 0.6.33 → 0.6.35

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.
@@ -1,5 +1,5 @@
1
- import { AnyClientTool, ModelMessage } from '@tanstack/ai';
2
- import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from '@tanstack/ai-client';
1
+ import { AnyClientTool, ModelMessage, SchemaInput } from '@tanstack/ai';
2
+ import { AIDevtoolsDisplayOptions, ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, DistributedOmit, MultimodalContent, UIMessage } from '@tanstack/ai-client';
3
3
  export type { ChatRequestBody, MultimodalContent, UIMessage };
4
4
  /**
5
5
  * Options for the useChat hook.
@@ -17,8 +17,17 @@ export type { ChatRequestBody, MultimodalContent, UIMessage };
17
17
  * Note: Connection and body changes will recreate the ChatClient instance.
18
18
  * To update these options, remount the component or use a key prop.
19
19
  */
20
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
20
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = DistributedOmit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange' | 'devtools'> & {
21
21
  live?: boolean;
22
+ /** Display options for TanStack AI Devtools. */
23
+ devtools?: AIDevtoolsDisplayOptions;
24
+ /**
25
+ * Standard-schema-compatible schema used to identify structured-output chat
26
+ * hooks in devtools. Preact currently exposes structured-output parts via
27
+ * `messages`; typed `partial` / `final` sugar is implemented in the other
28
+ * framework adapters.
29
+ */
30
+ outputSchema?: SchemaInput;
22
31
  };
23
32
  export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
24
33
  /**
@@ -1,4 +1,5 @@
1
1
  import { ChatClient } from "@tanstack/ai-client";
2
+ import { createChatDevtoolsBridge } from "@tanstack/ai-client/devtools";
2
3
  import { useId, useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
3
4
  function useChat(options) {
4
5
  const hookId = useId();
@@ -25,14 +26,22 @@ function useChat(options) {
25
26
  const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
26
27
  isFirstMountRef.current = false;
27
28
  const initialOptions = optionsRef.current;
29
+ const transport = initialOptions.connection ? { connection: initialOptions.connection } : { fetcher: initialOptions.fetcher };
28
30
  return new ChatClient({
29
- connection: initialOptions.connection,
31
+ devtoolsBridgeFactory: createChatDevtoolsBridge,
32
+ ...transport,
30
33
  id: clientId,
31
34
  initialMessages: messagesToUse,
32
35
  ...initialOptions.body !== void 0 && { body: initialOptions.body },
33
36
  ...initialOptions.forwardedProps !== void 0 && {
34
37
  forwardedProps: initialOptions.forwardedProps
35
38
  },
39
+ devtools: {
40
+ ...initialOptions.devtools,
41
+ framework: "preact",
42
+ hookName: "useChat",
43
+ outputKind: initialOptions.outputSchema ? "structured" : "chat"
44
+ },
36
45
  // Wrap every callback so the latest options are read at call time.
37
46
  // Capturing the function reference directly would freeze it to whatever
38
47
  // the parent passed on the first render.
@@ -95,14 +104,16 @@ function useChat(options) {
95
104
  }
96
105
  }, [client, options.live]);
97
106
  useEffect(() => {
107
+ client.mountDevtools();
98
108
  return () => {
99
- if (options.live) {
109
+ if (optionsRef.current.live) {
100
110
  client.unsubscribe();
101
111
  } else {
102
112
  client.stop();
103
113
  }
114
+ client.dispose();
104
115
  };
105
- }, [client, options.live]);
116
+ }, [client]);
106
117
  const sendMessage = useCallback(
107
118
  async (content) => {
108
119
  await client.sendMessage(content);
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type { ChatClientState, ConnectionStatus } 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<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): 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>>(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 return new ChatClient({\n connection: initialOptions.connection,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\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 })\n }, [client, options.body, options.forwardedProps])\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 return () => {\n if (options.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n }\n }, [client, options.live])\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 { ChatClientState, ConnectionStatus } 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<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): 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>>(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({\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 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 })\n }, [client, options.body, options.forwardedProps])\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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"}
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/ai-preact",
3
- "version": "0.6.33",
3
+ "version": "0.6.35",
4
4
  "description": "Preact hooks for TanStack AI streaming chat and typed messages.",
5
5
  "author": "",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/TanStack/ai.git",
10
- "directory": "packages/typescript/ai-preact"
10
+ "directory": "packages/ai-preact"
11
11
  },
12
12
  "type": "module",
13
13
  "module": "./dist/esm/index.js",
@@ -34,7 +34,7 @@
34
34
  "tool-calling"
35
35
  ],
36
36
  "dependencies": {
37
- "@tanstack/ai-client": "0.11.8"
37
+ "@tanstack/ai-client": "0.13.0"
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.22.0"
48
+ "@tanstack/ai": "^0.22.1"
49
49
  },
50
50
  "scripts": {
51
51
  "clean": "premove ./build ./dist",
package/src/types.ts CHANGED
@@ -1,9 +1,11 @@
1
- import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
1
+ import type { AnyClientTool, ModelMessage, SchemaInput } from '@tanstack/ai'
2
2
  import type {
3
+ AIDevtoolsDisplayOptions,
3
4
  ChatClientOptions,
4
5
  ChatClientState,
5
6
  ChatRequestBody,
6
7
  ConnectionStatus,
8
+ DistributedOmit,
7
9
  MultimodalContent,
8
10
  UIMessage,
9
11
  } from '@tanstack/ai-client'
@@ -28,7 +30,7 @@ export type { ChatRequestBody, MultimodalContent, UIMessage }
28
30
  * To update these options, remount the component or use a key prop.
29
31
  */
30
32
  export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
31
- Omit<
33
+ DistributedOmit<
32
34
  ChatClientOptions<TTools>,
33
35
  | 'onMessagesChange'
34
36
  | 'onLoadingChange'
@@ -37,8 +39,18 @@ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
37
39
  | 'onSubscriptionChange'
38
40
  | 'onConnectionStatusChange'
39
41
  | 'onSessionGeneratingChange'
42
+ | 'devtools'
40
43
  > & {
41
44
  live?: boolean
45
+ /** Display options for TanStack AI Devtools. */
46
+ devtools?: AIDevtoolsDisplayOptions
47
+ /**
48
+ * Standard-schema-compatible schema used to identify structured-output chat
49
+ * hooks in devtools. Preact currently exposes structured-output parts via
50
+ * `messages`; typed `partial` / `final` sugar is implemented in the other
51
+ * framework adapters.
52
+ */
53
+ outputSchema?: SchemaInput
42
54
  }
43
55
 
44
56
  export interface UseChatReturn<
package/src/use-chat.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ChatClient } from '@tanstack/ai-client'
2
+ import { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'
2
3
  import {
3
4
  useCallback,
4
5
  useEffect,
@@ -60,14 +61,25 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
60
61
  // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects
61
62
  // assigning `undefined` to those, so we omit the key when absent.
62
63
  const initialOptions = optionsRef.current
64
+ const transport = initialOptions.connection
65
+ ? { connection: initialOptions.connection }
66
+ : { fetcher: initialOptions.fetcher }
67
+
63
68
  return new ChatClient({
64
- connection: initialOptions.connection,
69
+ devtoolsBridgeFactory: createChatDevtoolsBridge,
70
+ ...transport,
65
71
  id: clientId,
66
72
  initialMessages: messagesToUse,
67
73
  ...(initialOptions.body !== undefined && { body: initialOptions.body }),
68
74
  ...(initialOptions.forwardedProps !== undefined && {
69
75
  forwardedProps: initialOptions.forwardedProps,
70
76
  }),
77
+ devtools: {
78
+ ...initialOptions.devtools,
79
+ framework: 'preact',
80
+ hookName: 'useChat',
81
+ outputKind: initialOptions.outputSchema ? 'structured' : 'chat',
82
+ },
71
83
  // Wrap every callback so the latest options are read at call time.
72
84
  // Capturing the function reference directly would freeze it to whatever
73
85
  // the parent passed on the first render.
@@ -151,14 +163,21 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
151
163
  // DO NOT include isLoading in dependencies - that would cause the cleanup
152
164
  // to run when isLoading changes, aborting continuation requests.
153
165
  useEffect(() => {
166
+ client.mountDevtools()
167
+
154
168
  return () => {
155
- if (options.live) {
169
+ // Subscribe/unsubscribe on `options.live` is owned by the dedicated
170
+ // effect above. This cleanup only fires on unmount or client swap,
171
+ // so read `live` through the ref to avoid disposing the client every
172
+ // time `live` toggles.
173
+ if (optionsRef.current.live) {
156
174
  client.unsubscribe()
157
175
  } else {
158
176
  client.stop()
159
177
  }
178
+ client.dispose()
160
179
  }
161
- }, [client, options.live])
180
+ }, [client])
162
181
 
163
182
  // All callback options are read through optionsRef at call time, so fresh
164
183
  // closures from each render are picked up without recreating the client.