Skip to Content
ExtendingReact SDKChat & Threads

Chat & Threads

useChat

The useChat() hook is the core hook for sending and receiving messages. It handles streaming, thread creation, message editing, and tool approvals.

New Conversation

Omit threadId to start a new conversation. Use onThreadCreated to capture the new thread ID:

import { useChat } from '@pandorakit/react-sdk' function NewChat() { const router = useRouter() const { messages, sendMessage, status } = useChat({ onThreadCreated: (threadId) => { router.push(`/chat/${threadId}`) }, }) return ( <div> {messages.map((msg) => ( <div key={msg.id}>{msg.role}: {msg.parts.filter(p => p.type === 'text').map(p => p.text).join('')}</div> ))} <form onSubmit={(e) => { e.preventDefault() const text = new FormData(e.currentTarget).get('message') as string sendMessage({ text }) e.currentTarget.reset() }}> <input name="message" placeholder="Send a message..." /> <button type="submit" disabled={status !== 'ready'}>Send</button> </form> </div> ) }

Existing Thread

Pass threadId and initialMessages (from useThread) to continue a conversation. The hook automatically resumes active streams:

import { useChat, useThread } from '@pandorakit/react-sdk' function ExistingChat({ threadId }: { threadId: string }) { const { data } = useThread(threadId) const { messages, sendMessage, status } = useChat({ threadId, initialMessages: data?.messages, }) // ... render messages and input }

Editing a Message

editMessage forks the conversation at the specified message and sends the edited text:

const { editMessage } = useChat({ threadId }) async function handleEdit(messageId: string, newText: string) { const newThreadId = await editMessage(messageId, newText) router.push(`/chat/${newThreadId}`) }

Tool Approvals

When the agent requests approval for a tool call, use addToolApprovalResponse:

const { messages, addToolApprovalResponse } = useChat({ threadId }) addToolApprovalResponse({ toolCallId, approved: true }) // or addToolApprovalResponse({ toolCallId, approved: false })

Error Handling

const { messages, error } = useChat({ threadId, onError: (err) => { toast.error(err.message || 'Something went wrong') }, })

UseChatOptions

PropertyTypeDescription
threadIdstringThread ID for existing conversations. Omit for new chats.
initialMessagesServerMessage[]Messages to populate the chat with (from useThread).
onThreadCreated(threadId: string) => voidCalled when the server creates a new thread.
onFinish() => voidCalled when the assistant response stream finishes.
onError(error: Error) => voidCalled on streaming errors.

UseChatReturn

PropertyTypeDescription
messagesUIMessage[]The conversation messages.
sendMessage(message: { text: string; files?: FileUIPart[] }) => Promise<void>Send a user message, optionally with files.
statusChatStatus'ready' | 'submitted' | 'streaming' | 'error'
stop() => Promise<void>Stop the current stream.
errorError | undefinedThe last error, if any.
setMessages(messages: UIMessage[] | ((msgs: UIMessage[]) => UIMessage[])) => voidManually set messages.
editMessage(messageId: string, newText: string) => Promise<string>Fork and send edited text. Returns new thread ID.
addToolApprovalResponseChatAddToolApproveResponseFunctionApprove or deny a pending tool call.

For rendering message part types, see Message Part Types.

useThreads

Lists all threads, with support for forking and deletion.

import { useThreads } from '@pandorakit/react-sdk' function ThreadSidebar() { const { data, remove } = useThreads() return ( <nav> {data?.threads.map((thread) => ( <div key={thread.id}> <a href={`/chat/${thread.id}`}>{thread.title || 'New Chat'}</a> <button onClick={() => remove(thread.id)}>Delete</button> </div> ))} </nav> ) }
PropertyTypeDescription
dataThreadListResponse | undefinedThread list including threads and activeStreamIds.
isLoadingbooleanWhether the initial fetch is in progress.
errorError | nullFetch error, if any.
fork(args: { threadId: string; messageId: string }) => Promise<ThreadForkResponse>Fork a thread at a specific message.
remove(threadId: string) => Promise<void>Delete a thread.

useThread

Fetches a single thread with its full message history and fork metadata.

import { useThread } from '@pandorakit/react-sdk' function ThreadView({ threadId }: { threadId: string }) { const { data, isLoading } = useThread(threadId) if (isLoading) return <div>Loading...</div> return ( <div> <h2>{data?.thread.title}</h2> {data?.messages.map((msg) => ( <div key={msg.id}>{/* render message */}</div> ))} </div> ) }
PropertyTypeDescription
dataThreadDetailResponse | undefinedThread details including thread, messages, forks, and forkInfo.
isLoadingbooleanWhether the fetch is in progress.
errorError | nullFetch error, if any.

Rendering Messages

The messages array from useChat() contains UIMessage objects with a parts array:

import type { UIMessage } from 'ai' function MessageParts({ message }: { message: UIMessage }) { return ( <> {message.parts.map((part, i) => { switch (part.type) { case 'text': return <p key={i}>{part.text}</p> case 'reasoning': return ( <details key={i}> <summary>Thinking...</summary> {part.text} </details> ) case 'file': return part.mediaType?.startsWith('image/') ? ( <img key={i} src={part.url} alt={part.filename || 'attachment'} /> ) : ( <a key={i} href={part.url} download>{part.filename || 'File'}</a> ) case 'source-url': return <a key={i} href={part.url}>{part.title || part.url}</a> default: if (part.type.startsWith('tool-')) { return ( <pre key={i}> Tool: {part.type} ({part.state}) {part.state === 'output-available' && JSON.stringify(part.output, null, 2)} </pre> ) } return null } })} </> ) }

See the Message Part Types reference for the full list.

Last updated on