@openuidev/react-headless
v0.7.9
Published
Headless React primitives for AI chat — state management, streaming adapters for OpenAI and AG-UI, message format converters, and thread management for OpenUI generative UI apps
Readme
@openuidev/react-headless
Headless React primitives for OpenUI — chat state management, streaming adapters, and message format converters. Build any chat UI while OpenUI handles the streaming, threading, and state.
Install
npm install @openuidev/react-headless
# or
pnpm add @openuidev/react-headlessPeer dependencies: react >=19.0.0, react-dom >=19.0.0, zustand ^4.5.5
Overview
@openuidev/react-headless gives you everything needed to build a chat experience without imposing any UI. It provides:
ChatProvider— A React context provider that manages threads, messages, and streaming state via a Zustand store.- Selector hooks —
useThread()anduseThreadList()to read and interact with chat state. - Streaming adapters — Parse SSE or SDK responses from OpenAI, AG-UI, or custom backends.
- Message formats — Convert between your API's message format and the internal AG-UI format.
Quick Start
URL-based setup
The simplest configuration — point to your API and the provider handles REST calls and streaming automatically:
import { ChatProvider } from "@openuidev/react-headless";
function App() {
return (
<ChatProvider
apiUrl="/api/chat"
threadApiUrl="/api/threads"
>
<YourChatUI />
</ChatProvider>
);
}Custom functions
For full control, provide your own functions instead of URLs:
<ChatProvider
processMessage={async ({ threadId, messages, abortController }) => {
return fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ threadId, messages }),
signal: abortController.signal,
});
}}
fetchThreadList={async () => {
const res = await fetch("/api/threads");
return res.json();
}}
createThread={async (firstMessage) => {
const res = await fetch("/api/threads", {
method: "POST",
body: JSON.stringify({ message: firstMessage }),
});
return res.json();
}}
>
<YourChatUI />
</ChatProvider>Hooks
useThread()
Access the current thread's messages, send new messages, and check streaming state:
import { useThread } from "@openuidev/react-headless";
function ChatMessages() {
const { messages, isRunning, processMessage, cancelMessage } = useThread();
const handleSend = (text: string) => {
processMessage({ role: "user", content: text });
};
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>{msg.content}</div>
))}
{isRunning && <button onClick={cancelMessage}>Stop</button>}
</div>
);
}Returns: ThreadState & ThreadActions
| Field | Type | Description |
| :--- | :--- | :--- |
| messages | Message[] | Messages in the current thread |
| isRunning | boolean | Whether the model is currently streaming |
| isLoadingMessages | boolean | Whether messages are being fetched |
| threadError | Error \| null | Error from the last operation |
| processMessage(msg) | (msg) => Promise<void> | Send a message and stream the response |
| cancelMessage() | () => void | Abort the current stream |
| appendMessages(...msgs) | (...msgs) => void | Append messages locally |
| updateMessage(msg) | (msg) => void | Update a message in place |
| deleteMessage(id) | (id) => void | Remove a message |
| setMessages(msgs) | (msgs) => void | Replace all messages |
useThreadList()
Manage multiple conversation threads:
import { useThreadList } from "@openuidev/react-headless";
function ThreadSidebar() {
const { threads, selectedThreadId, selectThread, switchToNewThread, deleteThread } =
useThreadList();
return (
<nav>
<button onClick={switchToNewThread}>New Chat</button>
{threads.map((t) => (
<div key={t.id} onClick={() => selectThread(t.id)}>
{t.title}
<button onClick={() => deleteThread(t.id)}>Delete</button>
</div>
))}
</nav>
);
}Returns: ThreadListState & ThreadListActions
| Field | Type | Description |
| :--- | :--- | :--- |
| threads | Thread[] | All loaded threads |
| selectedThreadId | string \| null | Currently selected thread |
| isLoadingThreads | boolean | Whether the thread list is loading |
| hasMoreThreads | boolean | Whether more threads can be loaded |
| loadThreads() | () => void | Fetch the thread list |
| loadMoreThreads() | () => void | Load the next page of threads |
| selectThread(id) | (id) => void | Select a thread |
| switchToNewThread() | () => void | Deselect and start a new conversation |
| createThread(msg) | (msg) => Promise<Thread> | Create a thread with a first message |
| updateThread(thread) | (thread) => void | Update thread metadata |
| deleteThread(id) | (id) => void | Delete a thread |
useMessage()
Access the current message inside a message component:
import { useMessage } from "@openuidev/react-headless";
function MessageBubble() {
const { message } = useMessage();
return <div className="bubble">{message.content}</div>;
}Streaming Adapters
Adapters transform HTTP responses into the internal event stream. Pass one to ChatProvider via streamProtocol:
import { ChatProvider, openAIAdapter } from "@openuidev/react-headless";
<ChatProvider apiUrl="/api/chat" streamProtocol={openAIAdapter}>
{children}
</ChatProvider>| Adapter | Description |
| :--- | :--- |
| agUIAdapter | Default — parses AG-UI SSE events (data: {json}\n) |
| openAIAdapter | Parses OpenAI Chat Completions streaming (ChatCompletionChunk) |
| openAIResponsesAdapter | Parses OpenAI Responses API streaming (ResponseStreamEvent) |
| openAIReadableStreamAdapter | Parses OpenAI SDK's Stream.toReadableStream() NDJSON output |
Custom adapter
Implement the StreamProtocolAdapter interface:
import type { StreamProtocolAdapter, AGUIEvent } from "@openuidev/react-headless";
const myAdapter: StreamProtocolAdapter = {
async *parse(response: Response): AsyncIterable<AGUIEvent> {
// parse the response stream and yield AGUIEvent objects
},
};Message Formats
Message formats convert between your API's message shape and the internal AG-UI format. Pass one to ChatProvider via messageFormat:
import { ChatProvider, openAIMessageFormat } from "@openuidev/react-headless";
<ChatProvider apiUrl="/api/chat" messageFormat={openAIMessageFormat}>
{children}
</ChatProvider>| Format | Description |
| :--- | :--- |
| identityMessageFormat | Default — no conversion (messages are already AG-UI format) |
| openAIMessageFormat | Converts to/from OpenAI ChatCompletionMessageParam[] |
| openAIConversationMessageFormat | Converts to/from OpenAI Responses API ResponseInputItem[] |
Custom format
Implement the MessageFormat interface:
import type { MessageFormat } from "@openuidev/react-headless";
const myFormat: MessageFormat = {
toApi: (messages) => messages.map(convertToMyFormat),
fromApi: (data) => data as Message[],
};Types
import type {
ChatProviderProps,
ChatStore,
Thread,
ThreadState,
ThreadActions,
ThreadListState,
ThreadListActions,
CreateMessage,
Message,
UserMessage,
AssistantMessage,
SystemMessage,
ToolMessage,
ToolCall,
FunctionCall,
MessageFormat,
StreamProtocolAdapter,
AGUIEvent,
EventType,
} from "@openuidev/react-headless";Documentation
Full documentation and guides are available at openui.com.
