Skip to content

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):

FeatureWire
Approval and question cardsChatMessageList 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 upChatInput state={chat.composerState}
Pictures and downloads of a loaded conversationChatMessageList resolveFileUrl={chat.resolveFileUrl}
A lone free-text question answered from the composeronSend calls chat.resolveAction when composerState === "answer" (see Approvals and Questions)
Files, progress, retryChatInput 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 Retryinterrupted={chat.interrupted}, onRetryInterrupted={chat.retryInterrupted}
Usage meter and limitchat.budget, chat.spentUsd, chat.updateBudget, and BudgetBanner (see Usage Limits)

Direct state intervention ​

useChat and useChatContext expose direct setters for advanced control:

  • setMessages
  • setConversation
  • setIsStreaming
  • setStreamingText
  • setProgressMessage
  • setPendingFiles
  • setActiveToolCalls
  • setError

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/useChat own active conversation state
  • let useChatHistory own list/pagination/delete concerns
  • call history.refresh() after onConversationCreated / onMessageReceived when 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.

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub