DEV Community

ZNY
ZNY

Posted on

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

`typescript
// 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([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const messagesEndRef = useRef(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 (

{messages.map(msg => (

{msg.role === 'user' ? 'You' : 'Claude'}

{String(children).replace(/\n$/, '')}

) : (

{children}

);
}
}}
>
{msg.content}


))}
{isLoading && ( Claude ...

)}
{error &&

{error}} setInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }} placeholder="Ask Claude..." disabled={isLoading} /> Send
);
}
`

Streaming vs Non-Streaming

`typescript
// 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

`typescript
// useChat hook
function useChat(initialMessages: Message[] = []) {
const [messages, setMessages] = useState(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 ;
}
`

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.

👉 Get started with ofox.ai

This article contains affiliate links.

Tags: react,javascript,ai,programming,webdev
Canonical URL: https://dev.to/zny10289

Top comments (0)