| 24 | } from './types' |
| 25 | |
| 26 | export function useChat< |
| 27 | TTools extends ReadonlyArray<AnyClientTool> = any, |
| 28 | TSchema extends SchemaInput | undefined = undefined, |
| 29 | TContext = InferredClientContext<TTools>, |
| 30 | >( |
| 31 | options: UseChatOptions<TTools, TSchema, TContext>, |
| 32 | ): UseChatReturn<TTools, TSchema> { |
| 33 | const hookId = useId() |
| 34 | const clientId = options.id || hookId |
| 35 | |
| 36 | const [messages, setMessages] = useState<Array<UIMessage<TTools>>>( |
| 37 | options.initialMessages || [], |
| 38 | ) |
| 39 | const [isLoading, setIsLoading] = useState(false) |
| 40 | const [error, setError] = useState<Error | undefined>(undefined) |
| 41 | const [status, setStatus] = useState<ChatClientState>('ready') |
| 42 | const [isSubscribed, setIsSubscribed] = useState(false) |
| 43 | const [connectionStatus, setConnectionStatus] = |
| 44 | useState<ConnectionStatus>('disconnected') |
| 45 | const [sessionGenerating, setSessionGenerating] = useState(false) |
| 46 | |
| 47 | type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>> |
| 48 | type Final = InferSchemaType<NonNullable<TSchema>> |
| 49 | |
| 50 | // Track current messages in a ref to preserve them when client is recreated |
| 51 | const messagesRef = useRef<Array<UIMessage<TTools>>>( |
| 52 | options.initialMessages || [], |
| 53 | ) |
| 54 | const isFirstMountRef = useRef(true) |
| 55 | const activeClientRef = useRef<ChatClient | null>(null) |
| 56 | const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>( |
| 57 | null, |
| 58 | ) |
| 59 | |
| 60 | // Update ref synchronously during render so it's always current when useMemo runs. |
| 61 | messagesRef.current = messages |
| 62 | |
| 63 | // Track current options in a ref to avoid recreating client when options change |
| 64 | const optionsRef = useRef<UseChatOptions<TTools, TSchema, TContext>>(options) |
| 65 | optionsRef.current = options |
| 66 | |
| 67 | // Create ChatClient instance with callbacks to sync state |
| 68 | const client = useMemo(() => { |
| 69 | const messagesToUse = options.initialMessages || [] |
| 70 | isFirstMountRef.current = false |
| 71 | |
| 72 | // Build options with conditional spreads for fields whose source |
| 73 | // type is `T | undefined` but the ChatClient target uses a strict |
| 74 | // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects |
| 75 | // assigning `undefined` to those, so we omit the key when absent. |
| 76 | const initialOptions = optionsRef.current |
| 77 | const transport = initialOptions.connection |
| 78 | ? { connection: initialOptions.connection } |
| 79 | : { fetcher: initialOptions.fetcher } |
| 80 | |
| 81 | const instance = new ChatClient<TTools, TContext>({ |
| 82 | devtoolsBridgeFactory: createChatDevtoolsBridge, |
| 83 | ...transport, |