Skip to content

Tool Call Rendering

By default, useChat only processes text_delta events. To display tool calls, you need two things:

  1. A custom onEvent handler to capture tool_call events
  2. A tool_call case in your rendering switch to display them

Provide onEvent to handle both text_delta and tool_call events:

const { messages, isLoading, sendMessage, stop } = useChat({
api: "http://localhost:8000/chat/",
body: { session_id: "3" },
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;
}
},
});

The tool_call part has two fields: tool_name (the function name) and argument (a JSON string with the call arguments). Render them by adding a tool_call case to your switch:

{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>
))}

The argument field is a JSON string. Use JSON.parse to format it as readable JSON.

A complete component that handles and renders tool calls:

import { useChat } from "@devscalelabs/react-sse-chat";
function Chat() {
const { messages, isLoading, sendMessage, stop } = useChat({
api: "http://localhost:8000/chat/",
body: { session_id: "3" },
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;
}
},
});
return (
<div>
{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>
))}
{isLoading && (
<button type="button" onClick={stop}>
Stop
</button>
)}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem(
"message",
) as HTMLInputElement;
sendMessage(input.value);
input.value = "";
}}
>
<input name="message" placeholder="Type a message..." />
<button type="submit" disabled={isLoading}>
Send
</button>
</form>
</div>
);
}

If your backend only emits text_delta events (no tool calling), you don’t need onEvent at all. The default handler takes care of it:

const { messages, sendMessage } = useChat({
api: "/chat",
});

See the Quick Example for a minimal text-only setup.

The built-in ToolCallPart type:

interface ToolCallPart {
type: "tool_call";
tool_name: string;
argument: string; // JSON string
}

If you need additional fields (e.g. tool_call_id, status), or your backend sends custom event types beyond text_delta and tool_call, see Custom Event Handling for extending both part types and event types with generics.