Package Exports
- @openuidev/react-headless
- @openuidev/react-headless/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@openuidev/react-headless) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@openuidev/react-headless
Headless React primitives, streaming adapters, and state management for OpenUI.
<<<<<<< nit/react-headless
Table of Contents
- @openuidev/react-headless
Architecture Overview
┌──────────────────────────────────────────────────────────────┐
│ <ChatProvider> │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Zustand Store (ChatStore) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌───────────────────────────┐ │ │
│ │ │ ThreadListSlice │ │ ThreadSlice │ │ │
│ │ │ │ │ │ │ │
│ │ │ • threads[] │ │ • messages[] │ │ │
│ │ │ • selectedId │ │ • isRunning │ │ │
│ │ │ • loadThreads() │ │ • processMessage() │ │ │
│ │ │ • createThread()│ │ • cancelMessage() │ │ │
│ │ │ • deleteThread()│ │ • appendMessages() │ │ │
│ │ │ • selectThread()│ │ • updateMessage() │ │ │
│ │ │ • ... │ │ • ... │ │ │
│ │ └─────────────────┘ └───────────┬───────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────────────────┼──────────────────┘ │
│ │ │
│ processMessage() calls: │
│ │ │
│ ┌─────────────────────────────────────▼──────────────────┐ │
│ │ Streaming Pipeline │ │
│ │ │ │
│ │ fetch(apiUrl) ──► StreamProtocolAdapter.parse() │ │
│ │ │ │ │
│ │ AG-UI Events (SSE) │ │
│ │ │ │ │
│ │ processStreamedMessage() │ │
│ │ │ │ │ │ │
│ │ createMsg updateMsg deleteMsg │ │
│ │ └─────────┴─────────┘ │ │
│ │ │ │ │
│ │ Zustand set(messages) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Hooks: useThread() · useThreadList() · useMessage() │
└──────────────────────────────────────────────────────────────┘Installation
pnpm add @openuidev/react-headlessPeer dependencies
pnpm add react react-dom zustand eventsource-parser tiny-invariantQuick Start
Minimal setup (URL-based)
import { ChatProvider, useThread, useThreadList } from "@openuidev/react-headless";
function App() {
return (
<ChatProvider apiUrl="/api/chat" threadApiUrl="/api/threads">
<ChatUI />
</ChatProvider>
);
}
function ChatUI() {
const { messages, processMessage, isRunning } = useThread();
const { threads, selectThread, loadThreads } = useThreadList();
// ... render your chat UI
}Custom backend functions
<ChatProvider
processMessage={async ({ threadId, messages, abortController }) => {
return fetch("/my/custom/endpoint", {
method: "POST",
body: JSON.stringify({ threadId, messages }),
signal: abortController.signal,
});
}}
fetchThreadList={async () => {
const res = await fetch("/my/threads");
return res.json(); // { threads: Thread[], nextCursor?: any }
}}
createThread={async (firstMessage) => {
const res = await fetch("/my/threads", {
method: "POST",
body: JSON.stringify({ message: firstMessage }),
});
return res.json(); // Thread
}}
>
<ChatUI />
</ChatProvider>Core Concepts
ChatProvider
The <ChatProvider> is the top-level React component that creates and owns the Zustand store. It accepts configuration for how to communicate with your backend (either URL strings for convention-based REST endpoints, or custom async functions) and makes the store available to all descendant components via React context.
The store is created once on mount via useState(() => createChatStore(config)), so config changes after mount are not picked up.
Hooks
The package exposes two primary hooks that select specific slices from the unified store:
useThread()— Returns the current thread's messages, loading/running state, and actions to send/edit/delete messages.useThreadList()— Returns the list of threads, selection state, and actions to create/select/delete/update threads.
Both hooks use Zustand's useShallow for automatic shallow-equality memoization, preventing unnecessary re-renders. They also support an optional selector function for fine-grained subscriptions:
// Only re-renders when messages change
const messages = useThread((s) => s.messages);
// Only re-renders when thread count changes
const count = useThreadList((s) => s.threads.length);Streaming
When processMessage() is called, the following pipeline executes:
- An optimistic user message is appended to the store immediately.
- If no thread is selected, one is created automatically (via
createThreadorthreadApiUrl), or an"ephemeral"ID is used. - The message is sent to the backend via
fetch(apiUrl, ...)or the customprocessMessagefunction. - The response is piped through a StreamProtocolAdapter (default: AG-UI SSE format) which yields
AGUIEventobjects. processStreamedMessage()consumes these events and translates them intocreateMessage,updateMessage, anddeleteMessagecalls that update the Zustand store in real-time.
Supported event types:
TEXT_MESSAGE_START— New assistant message beginsTEXT_MESSAGE_CONTENT— Text delta appended to the current messageTEXT_MESSAGE_END— Message streaming completeTOOL_CALL_START— A tool/function call beginsTOOL_CALL_ARGS— Argument delta for a tool callTOOL_CALL_END— Tool call streaming completeRUN_ERROR— Error during the stream
Message Formats
A MessageFormat is a bidirectional converter between the internal AG-UI message format and whatever format your backend expects. It has two methods:
toApi(messages)— Converts outgoing messages (sent to your backend)fromApi(data)— Converts incoming messages (loaded from your backend/storage)
Built-in formats:
| Format | Use Case |
|---|---|
identityMessageFormat |
No conversion (default). Backend uses AG-UI format directly. |
openAIMessageFormat |
OpenAI Chat Completions API (ChatCompletionMessageParam[]) |
openAIConversationMessageFormat |
OpenAI Responses/Conversations API (ResponseInputItem[]) |
Stream Protocol Adapters
A StreamProtocolAdapter parses a Response body into an AsyncIterable<AGUIEvent>. This decouples the streaming protocol from the business logic.
Built-in adapters:
| Adapter | Protocol |
|---|---|
agUIAdapter() |
AG-UI SSE format (default). Lines are data: {JSON}\n with [DONE] sentinel. |
openAIAdapter() |
OpenAI Chat Completions streaming format (ChatCompletionChunk). |
openAIResponsesAdapter() |
OpenAI Responses API streaming format (ResponseStreamEvent). |
openAIReadableStreamAdapter() |
OpenAI SDK's Stream.toReadableStream() NDJSON format (no SSE prefix). |
Module Breakdown
v2/ — Zustand Store & Provider
This is the heart of the package. The "v2" designation indicates it's the current API, replacing an earlier hook-based approach.
| File | Purpose |
|---|---|
createChatStore.ts |
Factory function that creates the unified Zustand store. Accepts config (URLs or custom functions) and returns a fully wired store with all thread list and thread actions. Contains default implementations for REST operations when threadApiUrl is provided. |
ChatProvider.tsx |
React component that wraps createChatStore in a context provider. Creates the store once on mount. |
ChatContext.ts |
React context definition + useChatStore() hook that retrieves the raw Zustand StoreApi. Throws if used outside <ChatProvider>. |
hooks.ts |
useThread() and useThreadList() — typed selector hooks that project the unified store into focused slices. Uses useShallow for memoization. |
types.ts |
All TypeScript types for the v2 API: Thread, ThreadState, ThreadActions, ThreadListState, ThreadListActions, ChatStore, ChatProviderProps. Also defines the discriminated union config types (ThreadApiConfig, ChatApiConfig). |
Store Internals
The store is a single flat Zustand store combining two logical slices:
ThreadListSlice manages the sidebar/thread list:
threads: Thread[]— sorted bycreatedAtdescendingisLoadingThreads/threadListError/hasMoreThreads— loading stateselectedThreadId— the active thread_nextCursor— internal pagination cursor- Actions:
loadThreads,loadMoreThreads,switchToNewThread,createThread,selectThread,updateThread,deleteThread
ThreadSlice manages the active conversation:
messages: Message[]— the current thread's messagesisRunning— whether a stream is in progressisLoadingMessages/threadError— loading state_abortController— internal abort controller for cancellation- Actions:
processMessage,appendMessages,updateMessage,setMessages,deleteMessage,cancelMessage
Key behaviors:
- Optimistic updates:
processMessageappends the user message instantly before the network call. - Auto thread creation: If no thread is selected when sending a message, the store auto-creates one (or uses
"ephemeral"if no create function is configured). - Abort on switch:
selectThreadandswitchToNewThreadcancel any in-flight stream viaAbortController. - Deduplication:
mergeThreadListuses aMapkeyed bythreadIdto merge paginated results without duplicates. - Pending state:
deleteThreadandupdateThreadsetisPending: trueon the thread while the operation is in flight.
stream/ — Streaming Engine
| File | Purpose |
|---|---|
processStreamedMessage.ts |
Core function that consumes an AsyncIterable<AGUIEvent> (from an adapter) and translates events into message CRUD operations. Manages a running currentMessage that accumulates text deltas and tool calls. |
adapters/ag-ui.ts |
Default adapter. Parses SSE lines (data: {...}\n) from a Response body and yields AGUIEvent objects. |
adapters/openai-completions.ts |
Parses OpenAI Chat Completions streaming chunks (ChatCompletionChunk) and translates them to AG-UI events. Handles content deltas, tool call start/args/end, and finish reasons. |
adapters/openai-responses.ts |
Parses OpenAI Responses API streaming events (ResponseStreamEvent) and translates them to AG-UI events. Handles output items, text deltas, function call arguments, and error events. |
adapters/openai-message-format.ts |
MessageFormat implementation for OpenAI Chat Completions. Bidirectional conversion between AG-UI Message[] and ChatCompletionMessageParam[]. Handles user (including multipart/image), assistant (including tool calls), tool, system, and developer messages. |
adapters/openai-conversation-message-format.ts |
MessageFormat implementation for OpenAI Responses/Conversations API. Converts AG-UI messages to ResponseInputItem[] (flattening assistant tool calls into sibling items) and converts ConversationItem[] back to AG-UI messages (grouping adjacent function_calls into assistant messages). |
types/ — Type Definitions
| File | Purpose |
|---|---|
message.ts |
Re-exports all message types from @ag-ui/core: Message, UserMessage, AssistantMessage, ToolMessage, SystemMessage, DeveloperMessage, ReasoningMessage, ActivityMessage, ToolCall, FunctionCall, InputContent, TextInputContent, BinaryInputContent. Also defines CreateMessage = Omit<UserMessage, "id">. |
messageFormat.ts |
Defines the MessageFormat interface (toApi/fromApi) and the default identityMessageFormat (pass-through). |
stream.ts |
Defines the StreamProtocolAdapter interface (a parse(response) → AsyncIterable<AGUIEvent> contract). Re-exports EventType enum and AGUIEvent type from @ag-ui/core. |
hooks/ — Message Context
| File | Purpose |
|---|---|
useMessage.tsx |
MessageContext, MessageProvider, and useMessage hook. Provides a React context for passing the current Message down to child components (used by react-ui when rendering individual messages). Uses useShallow for stable context values. |
API Reference
ChatProviderProps
The provider accepts a discriminated union config — you provide either URL strings or custom functions, but not both:
Thread API (choose one):
| Prop | Type | Description |
|---|---|---|
threadApiUrl |
string |
Base URL for convention-based REST endpoints (/get, /create, /delete/:id, /update/:id, /get/:id) |
| — or — | ||
fetchThreadList |
(cursor?) => Promise<{ threads, nextCursor? }> |
Custom function to load threads with pagination |
createThread |
(firstMessage) => Promise<Thread> |
Custom function to create a thread |
deleteThread |
(id) => Promise<void> |
Custom function to delete a thread |
updateThread |
(thread) => Promise<Thread> |
Custom function to update a thread |
loadThread |
(threadId) => Promise<Message[]> |
Custom function to load a thread's messages |
Chat API (choose one):
| Prop | Type | Description |
|---|---|---|
apiUrl |
string |
URL to POST messages to. Sends { threadId, messages } and expects an SSE stream response. |
| — or — | ||
processMessage |
({ threadId, messages, abortController }) => Promise<Response> |
Custom function to send messages. Must return a Response with a readable stream body. |
Optional:
| Prop | Type | Default | Description |
|---|---|---|---|
streamProtocol |
StreamProtocolAdapter |
agUIAdapter() |
How to parse the streaming response |
messageFormat |
MessageFormat |
identityMessageFormat |
How to convert messages to/from your backend's format |
useThread
function useThread(): ThreadState & ThreadActions;
function useThread<T>(selector: (state: ThreadState & ThreadActions) => T): T;State:
| Field | Type | Description |
|---|---|---|
messages |
Message[] |
Messages in the current thread |
isRunning |
boolean |
Whether a message is being streamed |
isLoadingMessages |
boolean |
Whether messages are being loaded from the backend |
threadError |
Error | null |
Error from the last operation |
Actions:
| Method | Signature | Description |
|---|---|---|
processMessage |
(msg: CreateMessage) => Promise<void> |
Send a user message and stream the response |
appendMessages |
(...msgs: Message[]) => void |
Append messages to the thread |
updateMessage |
(msg: Message) => void |
Replace a message by ID |
setMessages |
(msgs: Message[]) => void |
Replace all messages |
deleteMessage |
(id: string) => void |
Remove a message by ID |
cancelMessage |
() => void |
Abort the in-flight stream |
useThreadList
function useThreadList(): ThreadListState & ThreadListActions;
function useThreadList<T>(selector: (state: ThreadListState & ThreadListActions) => T): T;State:
| Field | Type | Description |
|---|---|---|
threads |
Thread[] |
All loaded threads, sorted by createdAt desc |
isLoadingThreads |
boolean |
Whether the thread list is loading |
threadListError |
Error | null |
Error from thread list operations |
selectedThreadId |
string | null |
Currently active thread ID |
hasMoreThreads |
boolean |
Whether more pages are available |
Actions:
| Method | Signature | Description |
|---|---|---|
loadThreads |
() => void |
Fetch the first page of threads |
loadMoreThreads |
() => void |
Fetch the next page (uses cursor) |
switchToNewThread |
() => void |
Deselect current thread, clear messages |
createThread |
(firstMessage: UserMessage) => Promise<Thread> |
Create a new thread |
selectThread |
(threadId: string) => void |
Select a thread and load its messages |
updateThread |
(thread: Thread) => void |
Update thread metadata (e.g., title) |
deleteThread |
(threadId: string) => void |
Delete a thread |
useMessage
function useMessage(): { message: Message };Returns the current message from MessageContext. Must be used inside a <MessageProvider>. This is primarily used by @openuidev/react-ui when rendering individual message components.
processStreamedMessage
function processStreamedMessage(params: {
response: Response;
createMessage: (msg: AssistantMessage) => void;
updateMessage: (msg: AssistantMessage) => void;
deleteMessage: (id: string) => void;
adapter?: StreamProtocolAdapter;
}): Promise<AssistantMessage | void>;Low-level utility that consumes an SSE response via the given adapter and calls the provided callbacks as the message is built up incrementally. Used internally by the store's processMessage action, but exported for advanced use cases.
Stream Adapters
import {
agUIAdapter,
openAIAdapter,
openAIReadableStreamAdapter,
openAIResponsesAdapter,
} from "@openuidev/react-headless";
// Use with ChatProvider
<ChatProvider streamProtocol={openAIAdapter()} apiUrl="/api/chat">
...
</ChatProvider>;Message Format Converters
import {
openAIMessageFormat,
openAIConversationMessageFormat,
identityMessageFormat,
} from "@openuidev/react-headless";
// For OpenAI Chat Completions backends
<ChatProvider messageFormat={openAIMessageFormat} streamProtocol={openAIAdapter()} apiUrl="/api/chat">
...
</ChatProvider>
// For OpenAI Responses API backends
<ChatProvider
messageFormat={openAIConversationMessageFormat}
streamProtocol={openAIResponsesAdapter()}
apiUrl="/api/chat"
>
...
</ChatProvider>Type Reference
Message Types (from @ag-ui/core)
| Type | Description |
|---|---|
Message |
Union of all message types |
UserMessage |
User-sent message (text or multipart content with images) |
AssistantMessage |
LLM response (text content + optional tool calls) |
ToolMessage |
Tool/function result message |
SystemMessage |
System prompt message |
DeveloperMessage |
Developer instruction message |
ReasoningMessage |
Chain-of-thought reasoning |
ActivityMessage |
Status/activity indicator |
ToolCall |
A tool call within an assistant message |
FunctionCall |
The function name + arguments within a ToolCall |
CreateMessage |
Omit<UserMessage, "id"> — used when sending new messages |
Store Types
| Type | Description |
|---|---|
Thread |
{ threadId, title, createdAt, isPending? } |
ThreadState |
{ messages, isRunning, isLoadingMessages, threadError } |
ThreadActions |
{ processMessage, appendMessages, updateMessage, setMessages, deleteMessage, cancelMessage } |
ThreadListState |
{ threads, isLoadingThreads, threadListError, selectedThreadId, hasMoreThreads } |
ThreadListActions |
{ loadThreads, loadMoreThreads, switchToNewThread, createThread, selectThread, updateThread, deleteThread } |
ChatStore |
ThreadListState & ThreadListActions & ThreadState & ThreadActions (plus internal fields) |
ChatProviderProps |
Configuration for <ChatProvider> |
Adapter Types
| Type | Description |
|---|---|
StreamProtocolAdapter |
{ parse(response: Response): AsyncIterable<AGUIEvent> } |
MessageFormat |
{ toApi(msgs: Message[]): unknown; fromApi(data: unknown): Message[] } |
AGUIEvent |
Event emitted by stream adapters (from @ag-ui/core) |
EventType |
Enum: TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, TOOL_CALL_START, TOOL_CALL_ARGS, TOOL_CALL_END, RUN_ERROR, etc. |
How It All Fits Together
- You wrap your app in
<ChatProvider>with your backend config. - The provider creates a Zustand store with all thread and message management baked in.
- Your UI components call
useThread()anduseThreadList()to read state and dispatch actions. - When a user sends a message,
processMessage():- Appends an optimistic user message
- Auto-creates a thread if needed
- Sends the request to your backend
- Streams the response through the adapter pipeline
- Updates the store in real-time as tokens arrive
- The
MessageFormatlayer handles serialization differences between AG-UI's internal format and your backend (OpenAI Completions, Responses API, or custom). - The
StreamProtocolAdapterlayer handles parsing differences between SSE protocols. Everything is cancellable via
AbortController— switching threads or callingcancelMessage()cleanly aborts in-flight streams.
Install
pnpm add @openuidev/react-headlessDocs
main
Detailed documentation is available at openui.com.