DeltaKitDeltaKit

Custom Event Handling

Handle custom SSE events beyond the default text_delta.

By default, useStreamChat only handles text_delta events. All other event types are silently ignored. To handle tool calls, reasoning, progress indicators, or any custom data, provide an onEvent callback.

Replacing the Default Handler

When you provide onEvent, it replaces the default handler entirely. You must handle text_delta yourself:

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

useStreamChat({
  api: "/api/chat",
  onEvent: (event, { appendText }) => {
    // Without this, text won't appear!
    if (event.type === "text_delta") {
      appendText(event.delta);
    }
  },
});

Event Helpers

The second argument to onEvent is an EventHelpers object with three methods:

  • appendText(delta). Append text to the last TextPart of the current assistant message.
  • appendPart(part). Push a new content part onto the current assistant message.
  • setMessages(updater). Direct React state setter for advanced mutations.

Handling Tool Calls

useStreamChat({
  api: "/api/chat",
  onEvent: (event, { appendText, appendPart, setMessages }) => {
    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;

      case "tool_result":
        setMessages((msgs) => {
          const last = msgs[msgs.length - 1];
          if (last?.role !== "assistant") return msgs;

          const updatedParts = last.parts.map((part) => {
            if (part.type === "tool_call" && part.callId === event.call_id) {
              return { ...part, result: event.output };
            }
            return part;
          });

          return [...msgs.slice(0, -1), { ...last, parts: updatedParts }];
        });
        break;
    }
  },
});

See Tool Call Rendering for how to display these in the UI.

Streaming Reasoning

Display chain-of-thought as it arrives from reasoning models (o1, o3, Kimi, etc.).

Recipe Available: For a complete implementation including backend deduplication, UI patterns, and best practices, see the Reasoning Events recipe.

Accumulation Required: Like text_delta, reasoning events arrive as small chunks. You must accumulate them into a single reasoning part rather than creating new parts for each chunk. See the example below.

Display chain-of-thought as it arrives:

useStreamChat({
  api: "/api/chat",
  onEvent: (event, { appendText, setMessages }) => {
    switch (event.type) {
      case "text_delta":
        appendText(event.delta);
        break;

      case "reasoning_delta":
        setMessages((msgs) => {
          const last = msgs[msgs.length - 1];
          if (last?.role !== "assistant") return msgs;

          const lastPart = last.parts[last.parts.length - 1];

          if (lastPart?.type === "reasoning") {
            const updatedParts = [
              ...last.parts.slice(0, -1),
              { ...lastPart, text: lastPart.text + event.delta },
            ];
            return [...msgs.slice(0, -1), { ...last, parts: updatedParts }];
          }

          return [
            ...msgs.slice(0, -1),
            {
              ...last,
              parts: [...last.parts, { type: "reasoning", text: event.delta }],
            },
          ];
        });
        break;
    }
  },
});

Type-Safe Custom Events

Define your own event types for compile-time checking by passing a type parameter to useStreamChat:

import { useStreamChat } from "@deltakit/react";
import type { SSEEvent, ContentPart } from "@deltakit/react";

interface ReasoningDeltaEvent {
  type: "reasoning_delta";
  delta: string;
}

interface ProgressEvent {
  type: "progress";
  step: string;
  percent: number;
}

type MyEvent = SSEEvent | ReasoningDeltaEvent | ProgressEvent;

useStreamChat<ContentPart, MyEvent>({
  api: "/api/chat",
  onEvent: (event, { appendText }) => {
    // TypeScript knows all possible event.type values
    switch (event.type) {
      case "text_delta":
        appendText(event.delta);
        break;
      case "reasoning_delta":
        // event.delta is typed as string
        break;
      case "progress":
        // event.step and event.percent are typed
        break;
    }
  },
});

Server-Side Event Format

Your server can send any JSON with a type field as an SSE event. DeltaKit's parser yields it as-is:

# Python/FastAPI
async def generate():
    yield f'data: {json.dumps({"type": "text_delta", "delta": "Hello"})}\n\n'
    yield f'data: {json.dumps({"type": "reasoning_delta", "delta": "Let me think..."})}\n\n'
    yield f'data: {json.dumps({"type": "progress", "step": "searching", "percent": 50})}\n\n'
    yield 'data: [DONE]\n\n'

There is no restriction on event types. Any valid JSON with a type field works. See SSE Events for the built-in event types and wire format.

On this page