DeltaKitDeltaKit

useStreamChat

The core React hook for streaming chat with pluggable transports.

useStreamChat manages the full lifecycle of a streaming chat: state, network requests, event parsing, cancellation, and event handling. It supports multiple transport strategies — direct SSE, background SSE, WebSocket, or a fully custom transport.

import { useStreamChat } from "@deltakit/react";

Signature

function useStreamChat<
  TPart extends { type: string } = ContentPart,
  TEvent extends { type: string } = SSEEvent,
>(options: UseStreamChatOptions<TPart, TEvent>): UseStreamChatReturn<TPart>

Options

transport

Type: "sse" | "background-sse" | "websocket" | ChatTransport<TPart, TEvent>

The transport strategy to use. Defaults to "sse".

// Direct SSE (default)
useStreamChat({ api: "/api/chat" });

// Background SSE — job-based with resumable event streams
useStreamChat({
  transport: "background-sse",
  transportOptions: {
    backgroundSSE: {
      startApi: "/api/jobs",
      eventsApi: (id) => `/api/jobs/${id}/events`,
    },
  },
});

// WebSocket
useStreamChat({
  transport: "websocket",
  transportOptions: {
    websocket: { url: "ws://localhost:8000/ws" },
  },
});

// Custom transport
useStreamChat({
  transport: {
    start: ({ context, message }) => { /* ... */ },
    resume: ({ context, runId }) => { /* ... */ },
  },
});

transportOptions

Type: TransportOptions<TEvent>

Configuration for built-in transports. Each transport reads from its own key:

  • transportOptions.sse — for the "sse" transport
  • transportOptions.backgroundSSE — for the "background-sse" transport
  • transportOptions.websocket — for the "websocket" transport

See the Background Task Streaming and WebSocket Chat recipes for full configuration details.

api

Type: string

The endpoint URL to POST messages to. Required when using the default "sse" transport (or use transportOptions.sse.api).

useStreamChat({ api: "/api/chat" });

initialMessages

Type: Message<TPart>[]

Pre-populate the conversation, e.g. from a database or fromOpenAiAgents.

useStreamChat({
  api: "/api/chat",
  initialMessages: [
    { id: "1", role: "user", parts: [{ type: "text", text: "Hello" }] },
    { id: "2", role: "assistant", parts: [{ type: "text", text: "Hi!" }] },
  ],
});

headers

Type: Record<string, string>

Extra headers merged into every fetch request. Content-Type: application/json is always included.

useStreamChat({
  api: "/api/chat",
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

body

Type: Record<string, unknown>

Extra fields merged into the POST body alongside message.

useStreamChat({
  api: "/api/chat",
  body: {
    model: "gpt-4",
    temperature: 0.7,
    sessionId: "abc123",
  },
});
// POST body: { message: "...", model: "gpt-4", temperature: 0.7, sessionId: "abc123" }

onEvent

Type: (event: TEvent, helpers: EventHelpers<TPart>) => void

Custom handler for each SSE event. When provided, this replaces the default text_delta handling entirely.

useStreamChat({
  api: "/api/chat",
  onEvent: (event, { appendText, appendPart }) => {
    switch (event.type) {
      case "text_delta":
        appendText(event.delta);
        break;
      case "tool_call":
        appendPart({
          type: "tool_call",
          tool_name: event.tool_name,
          argument: event.argument,
          callId: event.call_id,
        });
        break;
    }
  },
});

See EventHelpers for the full helpers API and Custom Event Handling for patterns.

onFinish

Type: (messages: Message<TPart>[]) => void

Called when the stream ends successfully. Receives the full message array.

useStreamChat({
  api: "/api/chat",
  onFinish: (messages) => {
    // Persist to database, analytics, etc.
    saveConversation(messages);
  },
});

onMessage

Type: (message: Message<TPart>) => void

Called when a new message is added. Fires for both user messages (immediately on send) and assistant messages (when the stream completes).

useStreamChat({
  api: "/api/chat",
  onMessage: (message) => {
    console.log(`New ${message.role} message:`, message.id);
  },
});

onError

Type: (error: Error) => void

Called on fetch or stream errors. Abort errors (from calling stop()) are swallowed and do not trigger this callback.

useStreamChat({
  api: "/api/chat",
  onError: (error) => {
    toast.error(error.message);
  },
});

Return Values

messages

Type: Message<TPart>[]

Chronological list of all messages. Updates in real-time during streaming.

isLoading

Type: boolean

true while streaming an assistant response. sendMessage is ignored while loading.

error

Type: Error | null

The most recent error, or null. Reset to null on each new sendMessage call.

runId

Type: string | null

The current resumable run id, if the active transport exposes one. Used by background-sse and websocket transports for reconnection. Reset to null when the stream completes.

sendMessage

Type: (text: string) => void

Send a user message and begin streaming the assistant response. Creates both a user message and an empty assistant message, then starts the SSE stream. Ignored if already loading.

stop

Type: () => void

Abort the current in-flight stream. Safe to call when not streaming. When cancelUrl (websocket) or cancelApi (background-sse) is configured, stop also sends a POST request to the backend to cancel the running job server-side.

setMessages

Type: Dispatch<SetStateAction<Message<TPart>[]>>

Direct React state setter. Use for programmatic manipulation:

const { setMessages } = useStreamChat({ api: "/api/chat" });

// Clear conversation
setMessages([]);

// Remove last message
setMessages((prev) => prev.slice(0, -1));

Generic Types

The hook accepts two type parameters for full type safety with custom content:

useStreamChat<TPart, TEvent>(options)
  • TPart. Shape of content parts in messages. Defaults to ContentPart. Must have { type: string }.
  • TEvent. Shape of SSE events from the server. Defaults to SSEEvent. Must have { type: string }.
interface StatusPart {
  type: "status";
  status: "thinking" | "searching";
}

interface StatusEvent {
  type: "status_update";
  status: "thinking" | "searching";
}

const { messages } = useStreamChat<
  ContentPart | StatusPart,
  SSEEvent | StatusEvent
>({
  api: "/api/chat",
  onEvent: (event, helpers) => {
    // event is SSEEvent | StatusEvent - fully typed
  },
});
// messages is Message<ContentPart | StatusPart>[]

Lifecycle

  1. sendMessage("Hello") is called
  2. A user message and an empty assistant message are appended to state
  3. onMessage fires for the user message
  4. isLoading becomes true
  5. The active transport starts (SSE fetch, background job, or WebSocket frame)
  6. Each event is passed to onEvent (or the default handler)
  7. State updates trigger re-renders with streaming content
  8. When the stream ends: onMessage fires for the assistant message, onFinish fires
  9. isLoading becomes false

Resume Lifecycle (background-sse / websocket)

When a persisted runId is found on mount:

  1. The transport's resume method is called with the run id
  2. isLoading becomes true
  3. Events stream from where the client left off
  4. The lifecycle continues from step 6 above

Cleanup

The hook automatically closes any in-flight transport (aborts fetch, closes WebSocket) when the component unmounts.

On this page