ChatIQ
ContactSolutionsDocsBlogDashboard
Sign inSign up
ContactSolutionsDocsBlogDashboard
Sign inSign up
ChatIQ

Build reliable AI chatbots powered by your team’s knowledge. Secure multi-tenant architecture, instant document ingestion, and guided analytics out of the box.

Product
How it worksFeaturesDemo
Resources
SolutionsBlogDocsContactDashboard
Legal & Preferences
Terms of ServicePrivacy PolicySecurityDPA
© 2026 ChatIQ. All rights reserved.Made with care in distributed workspaces worldwide.

React Chat Component Example

Complete React example for integrating ChatIQ chatbot with hooks and components.

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