useChatfor AG-UI streaming chatuseAgentfor direct agent invocationuseCompletionfor one-shot text generation
/api/ag-ui. Use the route from Chat UI or Agents, run veryfront dev, then open the page that renders the hook.
Prerequisites
- A page that can render React client components.
- An AG-UI route mounted at
/api/ag-ui(or another path you pass viaapi). - For
useCompletion, an API route that returns plain text or SSE for thecompletecall.
useChat
useChat exposes messages, input state, submit handlers, stop/reload handlers, model state, branch helpers, and inference status. It uses AG-UI for Veryfront AG-UI routes created with createAgUiHandler.
useAgent
UseuseAgent for direct agent invocation without the chat protocol:
useCompletion
Manage conversations
UseuseConversations when you need a conversation list, active selection, and
persistence without the preset sidebar. Handle every persistence failure
through onError, and map ConversationStoreError.operation to user-facing
copy:
save() calls for that id remain suppressed
until a later list confirms its absence, preventing late stream callbacks from
resurrecting it. To intentionally reuse the deleted id when that confirmation
cannot complete, call
conversations.save(replacement, { recreateDeleted: true }) explicitly.
conversations.isLoading covers only the initial summary-list request. Use
conversations.isActiveConversationLoading when the selected conversation’s
full record is being fetched. If that request fails,
activeConversationError identifies the exact conversation id; call
reloadActiveConversation() to retry it through the same provider-owned
store.
Load one conversation
UseuseConversation to load a full conversation without mounting the list
hook:
null means the conversation does
not exist. Read or decode failures populate error with a
ConversationStoreError whose operation is load.
Inside a ConversationsProvider, useConversation(id) reuses the provider
when id is active. Its loading state, load error, clearError(), and
reload() all remain attached to the provider’s store; an omitted or different
local storageKey is never consulted for that provider-owned id.
Choose conversation persistence
Omittingstore from either conversation hook uses
localConversationStore(storageKey).
Use local browser storage
Serve production pages from a secure browser context. The browser must provide the Web Locks API. The local adapter takes one exclusive lock for the logical store so cooperating same-origin tabs cannot overwrite the shared conversation index. If Web Locks are unavailable or inaccessible, the operation rejects before it touches storage. There is no unlocked fallback. Use the local adapter only when your data fits these maxima:- 2 MiB per serialized index and 4 MiB per serialized conversation
- 1,000 conversations per store, 1,000 messages per conversation, and 1,000 parts per message
- 1 KiB per identifier or storage-key component and 16 KiB per title
- 4 MiB per JSON string, nesting depth 64, 65,536 JSON nodes, and 10,000 entries per object or array
CONVERSATION_STORAGE_LIMITS object when application code
needs to inspect the same maxima before saving. Browser quota can be lower than
these codec limits. A save also needs temporary space for its bounded rollback
journal; a quota rejection leaves the prior record recoverable.
Every successful local save keeps a fixed-size idle reservation in the logical
store’s control slot. A current-format delete replaces that reservation with a
no-larger compact intent before removing data, so it can start without growing
the store at quota. Records written before this reservation protocol may still
need free space to establish it. Deleting a legacy record may also need room for
the current-format index tombstone used to suppress ambiguous legacy keys.
Conversation metadata, message metadata, tool input/output, and data parts must
be JSON-safe plain data. The adapter rejects undefined, bigint, symbols,
functions, non-finite numbers, negative zero, cyclic structures, accessors,
and objects with custom prototypes instead of silently changing them during
serialization. A Date is accepted only for message.createdAt and is stored
as an ISO string.
The adapter reads the legacy unversioned layout and migrates records on write.
Deleting a legacy record also removes its old blob when the decoded id proves
ownership. A malformed or cross-namespace legacy key cannot be attributed
safely, so deletion writes a current-format tombstone without deleting those
ambiguous bytes.
Treat this storage-format change as a coordinated rollout. Once a
current-format index is written, it is authoritative for that logical store;
older open tabs that continue writing legacy keys no longer update the view
seen by the new version, and an older rollback build does not understand the
new records. Require a reload or close older tabs before migration, and do not
roll back without an explicit data migration or export path.
Use memory for ephemeral sessions
Pass a stablememoryConversationStore() instance for SSR, tests, demos, or
sensitive sessions whose transcript must not enter Web Storage. Create it per
component tree with useState, as shown in
Keep conversations ephemeral.
The store does not survive a reload. Seed records are snapshotted at
construction and every seed id must be unique. Uncloneable seeds and duplicate
ids throw instead of being retained or overwritten. Unlike the local-storage
adapter, the in-memory adapter does not apply the durable storage codec at this
trusted, typed boundary.
Use a custom durable store
ImplementConversationStore with IndexedDB transactions when you need
stronger browser crash-atomic and multi-tab guarantees. Use an authenticated
API-backed store when conversations must follow a user across devices or need
server-side authorization, conflict handling, retention, or backups.
Pass the custom adapter through store and keep its object identity stable for
the mounted hook. list, load, save, and delete must return rejected
promises when they cannot complete. Implement subscribe only when the adapter
delivers out-of-band changes. It must throw when setup fails and return an
unsubscribe function after setup succeeds.
save must not resolve until a later list or load through the same store
instance can observe the accepted record. Those reads are authoritative and
may contain server-normalized titles, counts, or timestamps.
The hook calls that unsubscribe function when its subscription scope ends. It
never calls an injected store’s optional dispose, including on replacement or
unmount. The caller owns the store and must dispose it only after the final
consumer and any pending operations have finished.
Web Locks coordinate cooperating local writers. Before changing the
conversation blob and shared index, the local adapter records either a bounded
save before-image or a compact delete intent. The next locked operation rolls an
interrupted save back or finishes an interrupted delete before exposing stored
data. Recovery is fail-closed: a malformed control value or an unresolved
storage failure rejects the operation and retains its recovery state for retry.
Web Storage still does not provide a native multi-key transaction and cannot
coordinate old tabs or other writers that do not take the same lock. Concurrent
saves to the same conversation remain last-writer-wins, and a stale tab can save
after another tab deletes the conversation. Use IndexedDB or a durable custom
store with revisions, compare-and-set writes, and durable tombstones when the
application must prevent stale overwrites or deletion resurrection.
Composition hooks
When you compose the chat UI yourself (see UI + chat), these hooks expose the state behind the components. Context-backed hooks must be used inside their matching provider (<ChatInput> / <Message> / <Chat>), while
useChatScroll is standalone and can be used with any scroll container.
useChatInput
The headless composer. Reads the enclosing<ChatInput> context and returns the
input state plus prop-getters you spread onto your own elements. The getters
merge your handlers/classes with the internal ones:
getFormProps, getFieldProps (for a textarea), getSubmitProps, getAttachProps,
getVoiceProps. State: input, canSubmit, canAttach, isLoading, isListening,
canUseVoice, attachments, model. Use canAttach and canUseVoice to omit custom
controls when their corresponding capability is not configured. Their prop getters also
return fail-closed disabled state so unavailable controls cannot be re-enabled accidentally.
mergeProps is exported for composing several getters onto one element. The preset
<Chat> wires setInput automatically; direct <ChatInput> or <ChatInput.Root>
providers must receive setInput before a headless child calls input.setInput(...).
useChatScroll
Stick-to-bottom scroll management for a message list. AttachscrollRef to the
viewport and contentRef to the growing content; the hook keeps the user pinned
to the bottom while streaming:
viewportRef, isAtBottom, scrollToBottom/scrollToEnd, scrollToStart,
scrollToMessage(id), and getViewportProps(). (useStickToBottom is the old
name, kept as a deprecated alias.)
useMessageBranches
The regeneration/edit variants of a message (whatBranchPicker shows). Must be
used inside a <Message>:
Inference mode
useChat exposes inferenceMode so your UI can show whether inference is running through cloud, server-local, or browser runtime.
Verify it worked
Render the hook in a page and exercise the surface you care about:useChat: submit a message.chat.messagesshould grow andisLoadingshould flip while the response streams.useAgent: callinvoke.statusshould move throughrunningtoidleandmessagesshould contain the agent’s reply.useCompletion: callcomplete.completionshould populate andisLoadingshould flip back tofalsewhen the response ends.useConversations: create and rename a conversation, then reload. Ensure the local adapter restores the title. If an adapter rejects, ensure the alert identifies the operation and the UI does not claim the change is durable.useConversation: select a stored id. Ensure the hook returns the full conversation. Select Try again after a rejected load and confirm thatreloadstarts another request.memoryConversationStore: reload the page and confirm the conversation is gone.
isLoading never flips back, check the network tab for the request to
your API and the dev-server log for handler errors.