Chat - State and store
Configure the runtime via ChatProvider props, choose controlled or uncontrolled state, and explore the normalized store.
ChatProvider is the single entry point for the core runtime.
It creates the chat store, wires the adapter, and makes hooks and selectors available to every descendant component.
The following demo shows controlled state in action:
Controlled headless state
2
2
product
Document the controlled models.
The controlled API keeps public state array-first.
That is the behavior we want to document.
ChatProvider props
Required
| Prop | Type | Description |
|---|---|---|
adapter |
ChatAdapter<Cursor> |
The transport adapter |
children |
React.ReactNode |
Your UI tree |
Controlled and uncontrolled state
Each public state model supports both controlled and uncontrolled modes.
Use default* props to let the runtime own the value, or pass the value directly to control it from React state.
| Model | Controlled prop | Default prop | Change callback |
|---|---|---|---|
| Messages | messages |
initialMessages |
onMessagesChange |
| Conversations | conversations |
initialConversations |
onConversationsChange |
| Active conversation | activeConversationId |
initialActiveConversationId |
onActiveConversationChange |
| Composer value | composerValue |
initialComposerValue |
onComposerValueChange |
Callbacks
| Prop | Type | Description |
|---|---|---|
onToolCall |
(payload: ChatOnToolCallPayload) => void |
Called when a tool invocation state changes |
onFinish |
(payload: ChatOnFinishPayload) => void |
Called when a stream finishes, aborts, or fails |
onData |
(part: ChatDataMessagePart) => void |
Called when a data-* chunk arrives |
onError |
(error: ChatError) => void |
Called when any runtime error surfaces |
Configuration
| Prop | Type | Default | Description |
|---|---|---|---|
streamFlushInterval |
number |
16 |
Milliseconds between batched delta flushes |
partRenderers |
ChatPartRendererMap |
{} |
Custom renderers for message part types |
storeClass |
ChatStoreConstructor |
ChatStore |
Custom store class for advanced subclassing |
Controlled vs uncontrolled
Start uncontrolled
When prototyping or when the runtime can own the data, use default* props:
<ChatProvider
adapter={adapter}
initialActiveConversationId="support"
initialMessages={initialMessages}
>
<MyChat />
</ChatProvider>
The runtime manages the state internally and feeds it to hooks automatically.
Move to controlled
When you need to own the data externally — for example, to sync with a global store or persist across navigation — pass the state directly:
const [messages, setMessages] = React.useState<ChatMessage[]>([]);
const [activeId, setActiveId] = React.useState<string | undefined>('support');
<ChatProvider
adapter={adapter}
messages={messages}
onMessagesChange={setMessages}
activeConversationId={activeId}
onActiveConversationChange={setActiveId}
>
<MyChat />
</ChatProvider>;
The runtime still streams, normalizes, and derives selectors — you just own the source of truth.
You can switch from uncontrolled to controlled at any time without changing the runtime model.
Normalized internal state
The store keeps data in a normalized shape for efficient streaming and updates:
| Internal field | Type | Description |
|---|---|---|
messageIds |
string[] |
Ordered message IDs |
messagesById |
Record<string, ChatMessage> |
Message records by ID |
conversationIds |
string[] |
Ordered conversation IDs |
conversationsById |
Record<string, ChatConversation> |
Conversation records by ID |
activeConversationId |
string | undefined |
Active conversation |
typingByConversation |
Record<string, Record<string, boolean>> |
Typing state per conversation per user |
isStreaming |
boolean |
Whether a stream is active |
hasMoreHistory |
boolean |
Whether more history is available |
historyCursor |
Cursor | undefined |
Pagination cursor for history loading |
composerValue |
string |
Current draft text |
composerIsComposing |
boolean |
Whether an IME composition session is active |
composerAttachments |
ChatDraftAttachment[] |
File attachments in the draft |
error |
ChatError | null |
Current error state |
activeStreamAbortController |
AbortController | null |
Controller for aborting the active stream |
This normalization is why streaming updates are efficient — updating one message does not require rebuilding the entire thread array.
Error model
Runtime errors use the ChatError type:
interface ChatError {
code: string; // machine-readable error code
message: string; // human-readable description
source: ChatErrorSource; // where the error originated
recoverable: boolean; // whether the runtime can continue
retryable?: boolean; // whether the failed operation can be retried
details?: Record<string, unknown>; // additional context
}
type ChatErrorSource = 'send' | 'stream' | 'history' | 'render' | 'adapter';
Errors surface through:
useChat().erroruseChatStatus().erroronErrorcallback onChatProvider
Callbacks
onToolCall
Fires when a tool invocation state changes during streaming. Use it for side effects outside the message list — logging, analytics, or triggering external workflows.
interface ChatOnToolCallPayload {
toolCall: ChatToolInvocation | ChatDynamicToolInvocation;
}
onFinish
Fires when a stream reaches a terminal state (success, abort, disconnect, or error).
interface ChatOnFinishPayload {
message: ChatMessage; // the assistant message
messages: ChatMessage[]; // all messages after the stream
isAbort: boolean; // user stopped the stream
isDisconnect: boolean; // stream disconnected unexpectedly
isError: boolean; // stream ended with an error
finishReason?: string; // backend-provided reason
}
onData
Fires when a data-* chunk arrives.
Use it for transient data that should trigger app-level side effects without being persisted in the message.
Part renderer registration
Register custom renderers for message part types through the partRenderers prop:
const renderers: ChatPartRendererMap = {
'ticket-summary': ({ part }) => <div>Ticket: {part.ticketId}</div>,
};
<ChatProvider adapter={adapter} partRenderers={renderers}>
<MyChat />
</ChatProvider>;
Registered renderers are available through useChatPartRenderer(partType) inside any descendant component.
Custom store class
For advanced use cases, pass a custom store class via the storeClass prop.
The class must satisfy ChatStoreConstructor<Cursor>:
interface ChatStoreConstructor<Cursor = string> {
new (parameters: ChatStoreParameters<Cursor>): ChatStore<Cursor>;
}
Use this when you need to override internal normalization, add computed state, or integrate with an external store.
See also
- Hooks for the full hook API reference.
- Selectors for store selectors and advanced subscriptions.
- Controlled state for the controlled model pattern in action.
- Streaming lifecycle for send, stream, stop, and retry callbacks.