Target Keyword: "react ai chat component tutorial 2026"
Tags: react,javascript,ai,programming,webdev
Type: Tutorial
Content
Building React AI Chat Components in 2026: Complete Guide
Building a production-ready AI chat interface in React requires more than just displaying messages. You need streaming responses, markdown rendering, code highlighting, error handling, and a polished UX. Here's the complete implementation.
Core Chat Component
// ChatWindow.tsx
import React, { useState, useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
interface ChatWindowProps {
apiKey: string;
model?: string;
}
export function ChatWindow({ apiKey, model = 'claude-3-5-sonnet-20241022' }: ChatWindowProps) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isLoading]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content: input.trim(),
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://api.ofox.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model,
messages: [...messages, userMessage].map(m => ({
role: m.role,
content: m.content
})),
stream: true
})
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
// Handle streaming response
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantContent = '';
const assistantMessage: Message = {
id: crypto.randomUUID(),
role: 'assistant',
content: '',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessage]);
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.choices[0].delta.content) {
assistantContent += data.choices[0].delta.content;
setMessages(prev => prev.map(m =>
m.id === assistantMessage.id
? { ...m, content: assistantContent }
: m
));
}
}
}
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setMessages(prev => prev.filter(m => m.id !== userMessage.id));
} finally {
setIsLoading(false);
}
};
return (
<div className="chat-window">
<div className="messages">
{messages.map(msg => (
<div key={msg.id} className={`message ${msg.role}`}>
<div className="message-header">
{msg.role === 'user' ? 'You' : 'Claude'}
</div>
<div className="message-content">
<ReactMarkdown
components={{
code({ node, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const inline = !match;
return !inline ? (
<SyntaxHighlighter
language={match[1]}
PreTag="div"
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
}
}}
>
{msg.content}
</ReactMarkdown>
</div>
</div>
))}
{isLoading && (
<div className="message assistant">
<div className="message-header">Claude</div>
<div className="message-content typing">...</div>
</div>
)}
{error && <div className="error">{error}</div>}
<div ref={messagesEndRef} />
</div>
<div className="input-area">
<textarea
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder="Ask Claude..."
disabled={isLoading}
/>
<button onClick={sendMessage} disabled={isLoading || !input.trim()}>
Send
</button>
</div>
</div>
);
}
Streaming vs Non-Streaming
// Non-streaming (simpler, good for short responses)
async function chat(apiKey: string, messages: Message[]) {
const response = await fetch('https://api.ofox.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
messages: messages.map(m => ({ role: m.role, content: m.content }))
})
});
const data = await response.json();
return data.choices[0].message.content;
}
// Streaming (better UX for long responses)
async function* chatStream(apiKey: string, messages: Message[]) {
const response = await fetch('https://api.ofox.ai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
messages: messages.map(m => ({ role: m.role, content: m.content })),
stream: true
})
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split('\n')) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.choices[0].delta.content) {
yield data.choices[0].delta.content;
}
}
}
}
}
Building with Modern React Patterns
// useChat hook
function useChat(initialMessages: Message[] = []) {
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [isLoading, setIsLoading] = useState(false);
const send = async (content: string) => {
const userMessage: Message = {
id: crypto.randomUUID(),
role: 'user',
content,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setIsLoading(true);
// ... streaming logic
setIsLoading(false);
};
const clear = () => setMessages([]);
return { messages, send, clear, isLoading };
}
// Usage
function App() {
const { messages, send, isLoading } = useChat();
return <ChatWindow messages={messages} onSend={send} isLoading={isLoading} />;
}
Getting Started
Power your React chat app with ofox.ai — OpenAI-compatible API with Claude models. Sign up and get an API key to start building.
This article contains affiliate links.
Tags: react,javascript,ai,programming,webdev
Canonical URL: https://dev.to/zny10289
Top comments (0)