State Management
@cognipeer/chat-ui supports two state-management styles.
1) API-first (default)
Use Chat or ChatMinimal when you want a complete chat experience with minimal setup.
tsx
import { Chat } from "@cognipeer/chat-ui";
<Chat baseUrl="/api/agents" agentId="assistant" />;Best for:
- fast delivery
- standard chat layouts
- minimal custom orchestration
2) React-controlled (Context + Hooks)
Use ChatProvider + useChatContext when you want to control chat behavior directly from your React tree.
tsx
import {
ChatProvider,
useChatContext,
ChatMessageList,
ChatInput,
} from "@cognipeer/chat-ui";
function ChatContent() {
const chat = useChatContext();
return (
<>
<ChatMessageList
messages={chat.messages}
isStreaming={chat.isStreaming}
streamingText={chat.streamingText}
actions={{ onResolve: chat.resolveAction, canResolve: chat.canResolveActions }}
resolveFileUrl={chat.resolveFileUrl}
/>
<ChatInput
onSend={chat.sendMessage}
onStop={chat.stop}
isLoading={chat.isStreaming}
state={chat.composerState}
/>
</>
);
}
export default function Page() {
return (
<ChatProvider baseUrl="/api/agents" agentId="assistant">
<ChatContent />
</ChatProvider>
);
}Best for:
- custom page-level orchestration
- multi-panel/chat dashboards
- integrating chat state with local app state, analytics, feature flags
What a custom layout has to wire
Chat connects everything for you. A layout built from ChatMessageList and ChatInput passes these itself (the example above shows them):
| Feature | Wire |
|---|---|
| Approval and question cards | ChatMessageList actions={{ onResolve: chat.resolveAction, canResolve: chat.canResolveActions }}; without it the cards are read-only |
| Composer closed while a card waits, a turn runs elsewhere, a usage limit is used up | ChatInput state={chat.composerState} |
| Pictures and downloads of a loaded conversation | ChatMessageList resolveFileUrl={chat.resolveFileUrl} |
| A lone free-text question answered from the composer | onSend calls chat.resolveAction when composerState === "answer" (see Approvals and Questions) |
| Files, progress, retry | ChatInput pendingFiles, onFilesAdd={chat.addFiles}, onFileRemove, onFileRetry, isUploading |
| "Show earlier messages" | hasEarlier, isLoadingEarlier, onLoadEarlier from chat.hasEarlierMessages, chat.isLoadingEarlierMessages, chat.loadEarlierMessages |
| "The reply was interrupted" with Retry | interrupted={chat.interrupted}, onRetryInterrupted={chat.retryInterrupted} |
| Usage meter and limit | chat.budget, chat.spentUsd, chat.updateBudget, and BudgetBanner (see Usage Limits) |
Direct state intervention
useChat and useChatContext expose direct setters for advanced control:
setMessagessetConversationsetIsStreamingsetStreamingTextsetProgressMessagesetPendingFilessetActiveToolCallssetError
Use these for explicit override/sync flows. For standard message lifecycle, prefer sendMessage, retry, stop, loadConversation, clearMessages.
Composition with history
useChatHistory remains independent and composes with both styles.
Recommended pattern:
- let
ChatProvider/useChatown active conversation state - let
useChatHistoryown list/pagination/delete concerns - call
history.refresh()afteronConversationCreated/onMessageReceivedwhen needed
Which one should I choose?
- Start with API-first if you need a production chat quickly.
- Move to React-controlled mode when layout/control requirements grow.
- Keep both available in the same codebase: they are complementary, not mutually exclusive.

