Custom Components
useChat is intentionally un-opinionated about rendering. It gives you the data and controls — you bring the UI. This page shows common patterns for building custom chat components.
Custom Message Bubble
Section titled “Custom Message Bubble”Style messages differently based on role, and render each content part:
import { useChat } from "@devscalelabs/react-sse-chat";import type { Message } from "@devscalelabs/react-sse-chat";
function MessageBubble({ message }: { message: Message }) { const isUser = message.role === "user";
return ( <div style={{ alignSelf: isUser ? "flex-end" : "flex-start", background: isUser ? "#3b82f6" : "#e5e7eb", color: isUser ? "#fff" : "#1f2937", padding: "0.75rem 1rem", borderRadius: "1rem", maxWidth: "70%", }} > {message.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 Input Form
Section titled “Custom Input Form”Build a chat input with submit-on-enter and a send button:
import { useState } from "react";
function ChatInput({ onSend, isLoading,}: { onSend: (message: string) => void; isLoading: boolean;}) { const [value, setValue] = useState("");
const handleSubmit = () => { if (!value.trim() || isLoading) return; onSend(value); setValue(""); };
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }} > <input value={value} onChange={(e) => setValue(e.target.value)} placeholder="Type a message..." disabled={isLoading} /> <button type="submit" disabled={isLoading || !value.trim()}> Send </button> </form> );}Streaming Indicator
Section titled “Streaming Indicator”Show a typing indicator while the assistant is streaming:
function StreamingIndicator({ isLoading }: { isLoading: boolean }) { if (!isLoading) return null;
return ( <div style={{ display: "flex", gap: "0.25rem", padding: "0.5rem" }}> <span className="dot" /> <span className="dot" /> <span className="dot" /> </div> );}Putting It Together
Section titled “Putting It Together”Compose your custom components with useChat:
import { useChat } from "@devscalelabs/react-sse-chat";
function Chat() { const { messages, isLoading, sendMessage, stop } = useChat({ api: "/chat", });
return ( <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}> <div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: "0.5rem", padding: "1rem" }}> {messages.map((msg) => ( <MessageBubble key={msg.id} message={msg} /> ))} <StreamingIndicator isLoading={isLoading} /> </div>
{isLoading && ( <button onClick={stop} style={{ alignSelf: "center" }}> Stop generating </button> )}
<ChatInput onSend={sendMessage} isLoading={isLoading} /> </div> );}Since useChat returns standard React state (messages, isLoading) and simple callbacks (sendMessage, stop, setMessages), you can integrate it with any component library — Radix, shadcn/ui, Chakra, Material UI, or plain HTML.