fern/01-guide/08-frameworks/01-react-nextjs/02-chatbot.mdx
In this tutorial, you'll build a real-time streaming chatbot using BAML React hooks. By following along, you'll learn how to:
Before starting, ensure you have:
First, create a new BAML function for the chat completion:
<CodeBlocks> ```baml title="baml_src/chat.baml" class Message { role "user" | "assistant" content string }function Chat(messages: Message[]) -> string { client "openai/gpt-5-mini" prompt #" You are a helpful and knowledgeable AI assistant engaging in a conversation. Your responses should be: - Clear and concise - Accurate and informative - Natural and conversational in tone - Focused on addressing the user's needs
{{ ctx.output_format }}
{% for m in messages %}
{{ _.role(m.role)}}
{{m.content}}
{% endfor %}
"# }
test TestName { functions [Chat] args { messages [ { role "user" content "help me understand Chobani's success" } ] } }
</CodeBlocks>
Generate the BAML client to create the React hooks:
```bash
baml-cli generate
The useChat hook's data property contains the assistant's streaming response (a string), not the messages array. You need to maintain your own message state:
import { useChat } from "@/baml_client/react/hooks"; import { useState, useEffect } from "react"; import type { Message } from "@/baml_client/types";
export function ChatInterface() { const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState("");
const chat = useChat();
// When the assistant responds, add the response to the message history useEffect(() => { if (chat.isSuccess && chat.finalData) { setMessages((prev) => [ ...prev, { role: "assistant", content: chat.finalData! } ]); } }, [chat.isSuccess, chat.finalData]);
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!input.trim() || chat.isLoading) return;
const userMessage: Message = { role: "user", content: input };
const newMessages = [...messages, userMessage];
// Update local state immediately
setMessages(newMessages);
setInput("");
// Send the full message history to the Chat function
await chat.mutate(newMessages);
};
return ( <div> <div> {messages.map((message, i) => ( <div key={i}> <strong>{message.role}:</strong> {message.content} </div> ))} {chat.isLoading && ( <div> <strong>assistant:</strong> {chat.data ?? "Generating..."} </div> )} </div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
/>
<button type="submit" disabled={chat.isLoading}>
Send
</button>
</form>
</div>
); }
</CodeBlocks>
### Using Callbacks for Fine-Grained Control
You can use the hook's callbacks for more control over streaming events. This is useful for logging, analytics, or custom state management. The highlighted lines show the additions to the base example:
<CodeBlocks>
```tsx {9, 12-29, 54} title="app/components/chat-interface.tsx"
'use client'
import { useChat } from "@/baml_client/react/hooks";
import { useState } from "react";
import type { Message } from "@/baml_client/types";
export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [streamingContent, setStreamingContent] = useState("");
const [input, setInput] = useState("");
const chat = useChat({
// Called on each streaming partial update
onStreamData: (partial) => {
if (partial) {
setStreamingContent(partial);
}
},
// Called when streaming completes with the final response
onFinalData: (final) => {
if (final) {
setMessages((prev) => [
...prev,
{ role: "assistant", content: final }
]);
setStreamingContent("");
}
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || chat.isLoading) return;
const userMessage: Message = { role: "user", content: input };
const newMessages = [...messages, userMessage];
setMessages(newMessages);
setInput("");
await chat.mutate(newMessages);
};
return (
<div>
<div>
{messages.map((message, i) => (
<div key={i}>
<strong>{message.role}:</strong> {message.content}
</div>
))}
{chat.isLoading && (
<div>
<strong>assistant:</strong> {streamingContent || "Generating..."}
</div>
)}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
/>
<button type="submit" disabled={chat.isLoading}>
Send
</button>
</form>
</div>
);
}
With callbacks, you can:
onStreamDataonFinalDataonErrorTo enhance your chatbot, you could:
For more information, check out: