Open Kortyx on GitHub

React Client (@kortyx/react)

Updated 5 hours ago • May 22, 2026

@kortyx/react is the recommended client entry for React apps consuming streamed chat responses.

Start here when you want:

  • useChat(...) for batteries-included streamed chat state
  • useStructuredStreams() for custom React UIs without the full chat abstraction
  • route/server-action transport helpers
  • default browser storage for chat history

Use kortyx/browser only when you want raw stream readers or the low-level structured reducer outside React.

For most React apps, useChat(...) should be the first stop.

import { createRouteChatTransport, useChat } from "@kortyx/react"; export function ChatPage() { const chat = useChat({ transport: createRouteChatTransport({ endpoint: "/api/chat", }), }); return <ChatWindow chat={chat} />; }

useChat(...) owns:

  • finalized messages
  • live streamContentPieces
  • isStreaming
  • error and clearError()
  • abort() and canAbort
  • interrupt resume handling
  • structured stream accumulation
  • default browser storage unless you override it

Messages and active streams

messages contains finalized chat history only. While the assistant is still streaming, render the active assistant response from streamContentPieces.

This separation is intentional:

  • persisted history does not change on every token
  • active text, structured data, interrupts, and errors can render before finalization
  • finalized assistant messages can keep their debug chunks after the stream ends

When a stream finishes, useChat(...) builds the assistant message and appends it to messages.

Request context and message preparation

Pass context when the client should send non-message request data, such as a selected tenant id, visible thread id, or UI state needed by the server.

const chat = useChat({ transport: createRouteChatTransport({ endpoint: "/api/chat", }), context: { threadId, tenantId, }, });

By default, useChat(...) sends finalized history plus the outgoing user message. Use prepareContextMessages when your app owns a different history strategy, such as server-side history, summaries, or key facts.

const chat = useChat({ transport: createRouteChatTransport({ endpoint: "/api/chat", }), context: { threadId, tenantId, }, prepareContextMessages: async ({ messages, context }) => { const summary = await summarizeForThread(context.threadId, messages); return [ { role: "system", content: summary, }, ]; }, });

prepareContextMessages returns the context/history messages only. useChat(...) appends the outgoing user or resume message automatically.

Good to know: Client context is request metadata, not trusted authorization state. Authenticate the route, rate-limit it, and derive sensitive values such as user id or permissions on the server.

Inside nodes, read server-approved request context with useRuntimeContext(...).

import { useRuntimeContext } from "kortyx"; type AppContext = { userId: string; tenantId?: string; }; export async function supportNode() { const context = useRuntimeContext<AppContext>(); return `User: ${context.userId}`; }

See Runtime Context for the full client-to-route-to-node flow.

Runtime controls

useChat(...) exposes controls for common chat lifecycle cases:

  • abort() stops the active stream when the transport supports AbortSignal
  • canAbort is true while a stream is active
  • error stores the latest transport or stream error
  • clearError() clears that error state
  • clearMessages() clears visible/persisted messages and keeps the current session
  • resetSession() clears the current session id
  • resetChat() clears messages, active stream state, errors, and session id

clearChat() remains available as a compatibility alias for resetting the chat.

Good to know: clearMessages() is the right choice for a "clear visible history" button. Use resetChat() when you want a new local chat session.

Interrupt responses

For low-level control, call respondToHumanInput(...) with the resume token and request id.

For UI components rendering a HumanInputPiece, use respondToInterrupt(...) so the component can pass back the same interrupt piece it received.

respondToInterrupt(piece, { selected }) handles choice and multi-choice interrupts. respondToInterrupt(piece, { text }) handles text interrupts.

When the server sets interrupt routing metadata, HumanInputPiece preserves schemaId, schemaVersion, interruptId, and public meta. Switch on piece.schemaId for custom controls such as job pickers, file uploaders, or address autocompletes.

Abort support

Route transports created with createRouteChatTransport(...) receive the AbortSignal from useChat(...) and pass it to fetch.

Custom transports should forward context.signal to their own request layer if they want abort() to stop the active stream.

useChat(...) options

import type { ChatMsg, ChatStorage, ChatTransport, OutgoingChatMessage, ToHumanInputPiece, } from "@kortyx/react"; type UseChatOptions = { transport: ChatTransport<Record<string, unknown>>; storage?: ChatStorage<ChatMsg>; createId?: () => string; context?: Record<string, unknown>; prepareContextMessages?: (args: { messages: ChatMsg[]; sessionId: string; workflowId: string; reason: "send" | "resume"; context: Record<string, unknown>; }) => OutgoingChatMessage[] | Promise<OutgoingChatMessage[]>; toHumanInputPiece?: ToHumanInputPiece; };

Notes:

  • transport is required
  • storage defaults to browser storage
  • createId is optional when you want custom message/piece ids
  • context defaults to {}
  • prepareContextMessages defaults to the finalized message history when includeHistory is true
  • toHumanInputPiece lets advanced clients customize interrupt projection before pieces enter streamContentPieces and finalized assistant messages

Transport helpers

API route / SSE transport

import { createRouteChatTransport } from "@kortyx/react"; const transport = createRouteChatTransport({ endpoint: "/api/chat", createBody: ({ sessionId, workflowId, messages, context }) => ({ sessionId, workflowId, messages, context, }), });

Use this when your backend returns SSE chunks from a route handler. createBody is optional; without it the route body is { sessionId, workflowId, messages, context }.

Custom transport

If your app does not use an SSE route, provide your own transport.

import { createChatTransport } from "@kortyx/react"; const transport = createChatTransport({ stream: ({ sessionId, workflowId, messages }) => runChat({ sessionId, workflowId, messages, }), });

Use this when you already have an app-specific function that returns chunks.

Storage

By default, useChat(...) uses browser storage.

If you want to provide storage explicitly:

import { createBrowserChatStorage } from "@kortyx/react"; const storage = createBrowserChatStorage();

You can also provide your own ChatStorage implementation for app APIs, databases, or hybrid sync strategies.

import type { ChatStorage } from "@kortyx/react"; const storage: ChatStorage = { async load() { return {}; }, async save(state) { await fetch("/api/chat-state", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(state), }); }, async clearMessages() { await fetch("/api/chat-state/messages", { method: "DELETE", }); }, };

Custom React UI: useStructuredStreams()

If you want structured stream state without the full chat abstraction, use useStructuredStreams().

import { useStructuredStreams } from "@kortyx/react"; import { readStream } from "kortyx/browser"; export function StructuredPanel() { const structured = useStructuredStreams<Record<string, unknown>>(); async function load() { const response = await fetch("/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages }), }); for await (const chunk of readStream(response.body)) { structured.applyStreamChunk(chunk); if (chunk.type === "done") { break; } } } return null; }

That gives you:

  • items: stable ordered structured pieces
  • byStreamId: record-style access
  • get(streamId): direct lookup
  • clear(): reset between runs

When to drop to kortyx/browser

Use kortyx/browser when:

  • you are not using React
  • you want raw readStream(...) / consumeStream(...)
  • you want the low-level applyStructuredChunk(...) reducer directly
import { applyStructuredChunk, readStream } from "kortyx/browser";

That path is lower-level by design. React apps should usually start with @kortyx/react and only drop down when they need custom wiring.