Custom Event Handling
By default, only text_delta events are processed (appended as text parts to the assistant message). Use onEvent to handle additional event types.
Basic Usage
Section titled “Basic Usage”const { messages } = useChat({ api: "/chat", onEvent: (event, helpers) => { switch (event.type) { case "text_delta": helpers.appendText(event.delta); break; case "tool_call": helpers.appendPart({ type: "tool_call", tool_name: event.tool_name, argument: event.argument, }); break; } },});Helpers
Section titled “Helpers”The helpers object provides three methods:
appendText(delta: string)
Section titled “appendText(delta: string)”Append text to the last text part of the current assistant message. If the last part is not a text part, a new text part is created:
helpers.appendText(event.delta);appendPart(part: TPart)
Section titled “appendPart(part: TPart)”Append a new content part to the current assistant message. The type of part matches the generic parameter passed to useChat:
helpers.appendPart({ type: "tool_call", tool_name: event.tool_name, argument: event.argument,});setMessages(setter)
Section titled “setMessages(setter)”Full access to the React state setter for advanced mutations. Use this when you need to modify messages in ways that appendText and appendPart don’t support:
helpers.setMessages((prev) => { const last = prev[prev.length - 1]; // Custom mutation logic return [...prev.slice(0, -1), { ...last, parts: [{ type: "text", text: "replaced" }] }];});Rendering Parts
Section titled “Rendering Parts”Each message contains a parts array. Render them by switching on part.type:
{messages.map((msg) => ( <div key={msg.id}> <strong>{msg.role}:</strong> {msg.parts.map((part, i) => { switch (part.type) { case "text": return <span key={i}>{part.text}</span>; case "tool_call": return ( <pre key={i} style={{ opacity: 0.7, fontSize: "0.85em" }}> [Tool: {part.tool_name}]{"\n"} {JSON.stringify(JSON.parse(part.argument), null, 2)} </pre> ); default: return null; } })} </div>))}Custom Event Types
Section titled “Custom Event Types”The hook accepts a second generic parameter TEvent for custom SSE event types. Your backend might send events beyond text_delta and tool_call — for example, error, thinking, or status events. Define your custom event types and pass them as the second generic:
import { useChat } from "@devscalelabs/react-sse-chat";import type { SSEEvent } from "@devscalelabs/react-sse-chat";
// Define your custom eventinterface ErrorEvent { type: "error"; message: string;}
// Union with the built-in eventstype MyEvent = SSEEvent | ErrorEvent;
function Chat() { const { messages } = useChat<ContentPart, MyEvent>({ api: "/chat", onEvent: (event, helpers) => { switch (event.type) { case "text_delta": helpers.appendText(event.delta); break; case "tool_call": helpers.appendPart({ type: "tool_call", tool_name: event.tool_name, argument: event.argument, }); break; case "error": // event.message is fully typed here helpers.appendText(`[Error: ${event.message}]`); break; } }, });}Without the TEvent generic, TypeScript would reject case "error" because SSEEvent only includes "text_delta" | "tool_call".
Custom Part Types
Section titled “Custom Part Types”The first generic parameter TPart lets you define custom content part types. Your custom type must extend { type: string }:
import { useChat } from "@devscalelabs/react-sse-chat";import type { ContentPart, SSEEvent } from "@devscalelabs/react-sse-chat";
// Define custom part and eventinterface ThinkingPart { type: "thinking"; text: string;}
interface ThinkingEvent { type: "thinking"; text: string;}
type MyPart = ContentPart | ThinkingPart;type MyEvent = SSEEvent | ThinkingEvent;
function Chat() { const { messages } = useChat<MyPart, MyEvent>({ api: "/chat", onEvent: (event, helpers) => { switch (event.type) { case "text_delta": helpers.appendText(event.delta); break; case "tool_call": helpers.appendPart({ type: "tool_call", tool_name: event.tool_name, argument: event.argument, }); break; case "thinking": helpers.appendPart({ type: "thinking", text: event.text, }); break; } }, });
return ( <div> {messages.map((msg) => ( <div key={msg.id}> {msg.parts.map((part, i) => { switch (part.type) { case "text": return <span key={i}>{part.text}</span>; case "tool_call": return ( <pre key={i} style={{ opacity: 0.7, fontSize: "0.85em" }}> [Tool: {part.tool_name}]{"\n"} {JSON.stringify(JSON.parse(part.argument), null, 2)} </pre> ); case "thinking": return <details key={i}><summary>Thinking...</summary>{part.text}</details>; default: return null; } })} </div> ))} </div> );}Both generics are optional and default to the built-in types. All callbacks (onMessage, onFinish) and the returned messages array are fully typed with your custom part type.
Lifecycle Callbacks
Section titled “Lifecycle Callbacks”In addition to onEvent, you can use these callbacks for observing the chat lifecycle:
onMessage
Section titled “onMessage”Called when a complete message is added (both user and assistant):
useChat({ api: "/chat", onMessage: (message) => { console.log(`New ${message.role} message:`, message.parts); },});onFinish
Section titled “onFinish”Called when the stream ends with the full message array:
useChat({ api: "/chat", onFinish: (messages) => { console.log("Stream complete. Total messages:", messages.length); },});onError
Section titled “onError”Called when a fetch or stream error occurs:
useChat({ api: "/chat", onError: (error) => { console.error("Chat error:", error.message); },});