React Chat Component Example
A complete React implementation showing how to build a chat interface with ChatIQ using custom hooks and reusable components.
Overview
This example demonstrates:
- Custom React hooks for chat functionality
- Reusable chat components
- Streaming response handling
- Conversation state management
- TypeScript support
Best for: React applications, component libraries, production apps
Step 1: Custom Chat Hook
Create a custom hook to manage chat state and API calls:
// hooks/useChatIQ.ts
import { useState, useCallback } from 'react';
interface Message {
role: 'user' | 'assistant';
content: string;
}
interface UseChatIQOptions {
apiUrl?: string;
initialMessages?: Message[];
}
export function useChatIQ(options: UseChatIQOptions = {}) {
const { apiUrl = '/api/chatbot', initialMessages = [] } = options;
const [messages, setMessages] = useState<Message[]>(initialMessages);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [conversationId, setConversationId] = useState<string | null>(null);
const sendMessage = useCallback(async (message: string) => {
if (isLoading || !message.trim()) return;
setIsLoading(true);
setError(null);
// Add user message immediately
const userMessage: Message = { role: 'user', content: message };
setMessages((prev) => [...prev, userMessage]);
// Create placeholder for assistant response
const assistantMessageId = Date.now();
setMessages((prev) => [...prev, { role: 'assistant', content: '' }]);
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
conversation_id: conversationId,
stream: true,
}),
});
if (!response.body) {
throw new Error('No response body');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
let receivedConversationId: string | null = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') {
if (receivedConversationId) {
setConversationId(receivedConversationId);
}
break;
}
try {
const parsed = JSON.parse(data);
if (parsed.conversationId) {
receivedConversationId = parsed.conversationId;
}
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
fullResponse += content;
setMessages((prev) => {
const updated = [...prev];
updated[updated.length - 1] = {
role: 'assistant',
content: fullResponse,
};
return updated;
});
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error');
setError(error);
setMessages((prev) => [
...prev.slice(0, -1),
{
role: 'assistant',
content: 'Sorry, something went wrong. Please try again.',
},
]);
} finally {
setIsLoading(false);
}
}, [apiUrl, conversationId, isLoading]);
const clearMessages = useCallback(() => {
setMessages([]);
setConversationId(null);
setError(null);
}, []);
return {
messages,
sendMessage,
clearMessages,
isLoading,
error,
conversationId,
};
}
Step 2: Chat Component
Create a reusable chat component:
// components/ChatWidget.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useChatIQ } from '../hooks/useChatIQ';
export function ChatWidget() {
const { messages, sendMessage, isLoading, error } = useChatIQ();
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
sendMessage(input);
setInput('');
};
return (
<div className="chat-container">
<div className="chat-messages">
{messages.length === 0 ? (
<div className="empty-state">
<p>Start a conversation...</p>
</div>
) : (
messages.map((msg, idx) => (
<div key={idx} className={`message ${msg.role}`}>
<div className="message-content">{msg.content}</div>
</div>
))
)}
{isLoading && messages.length > 0 && (
<div className="message assistant">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
)}
{error && (
<div className="error-message">
Error: {error.message}
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={handleSubmit} className="chat-input">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
disabled={isLoading}
/>
<button type="submit" disabled={isLoading || !input.trim()}>
{isLoading ? 'Sending...' : 'Send'}
</button>
</form>
</div>
);
}
Step 3: Styling
Add CSS for the chat component:
/* styles/chat.css */
.chat-container {
display: flex;
flex-direction: column;
max-width: 600px;
height: 600px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f9f9f9;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.message {
margin-bottom: 15px;
padding: 12px;
border-radius: 8px;
max-width: 80%;
word-wrap: break-word;
}
.message.user {
background: #007bff;
color: white;
margin-left: auto;
text-align: right;
}
.message.assistant {
background: white;
border: 1px solid #ddd;
margin-right: auto;
}
.message-content {
white-space: pre-wrap;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 8px;
}
.typing-indicator span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #999;
animation: typing 1.4s infinite;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.7;
}
30% {
transform: translateY(-10px);
opacity: 1;
}
}
.chat-input {
display: flex;
padding: 15px;
background: white;
border-top: 1px solid #ddd;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.chat-input input:focus {
outline: none;
border-color: #007bff;
}
.chat-input button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.chat-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
.error-message {
padding: 10px;
background: #fee;
color: #c33;
border-radius: 4px;
margin-bottom: 15px;
}
Step 4: Usage in App
Use the component in your React app:
// App.tsx
import React from 'react';
import { ChatWidget } from './components/ChatWidget';
import './styles/chat.css';
function App() {
return (
<div className="App">
<header>
<h1>ChatIQ React Example</h1>
</header>
<main>
<ChatWidget />
</main>
</div>
);
}
export default App;
Step 5: Advanced Features
Add Message Timestamps
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp?: Date;
}
// In useChatIQ hook
const userMessage: Message = {
role: 'user',
content: message,
timestamp: new Date(),
};
Add Conversation History Persistence
// Save to localStorage
useEffect(() => {
if (messages.length > 0) {
localStorage.setItem('chatMessages', JSON.stringify(messages));
localStorage.setItem('conversationId', conversationId || '');
}
}, [messages, conversationId]);
// Load from localStorage
useEffect(() => {
const saved = localStorage.getItem('chatMessages');
const savedId = localStorage.getItem('conversationId');
if (saved) {
setMessages(JSON.parse(saved));
if (savedId) setConversationId(savedId);
}
}, []);
Add Message Actions (Copy, Retry)
// components/MessageActions.tsx
export function MessageActions({ message, onRetry }: {
message: Message;
onRetry?: () => void;
}) {
const copyToClipboard = () => {
navigator.clipboard.writeText(message.content);
};
return (
<div className="message-actions">
<button onClick={copyToClipboard}>Copy</button>
{message.role === 'assistant' && onRetry && (
<button onClick={onRetry}>Retry</button>
)}
</div>
);
}
TypeScript Types
Create a types file for better type safety:
// types/chat.ts
export interface Message {
role: 'user' | 'assistant';
content: string;
timestamp?: Date;
}
export interface ChatIQOptions {
apiUrl?: string;
botSlug?: string;
apiKey?: string;
stream?: boolean;
}
export interface ChatIQResponse {
response: string;
conversationId: string;
}
Next Steps
- See Next.js Integration for server components
- Add streaming support for real-time updates
- Implement message reactions or feedback
- Add file upload support
- Customize with your design system
See Also
- React Integration Guide - Complete integration guide
- Basic Chat Example - Vanilla JavaScript version
- Streaming Example - Streaming implementation details