ChatIQ
SolutionsSchools DemoChannelsDocsBlog
Sign inRequest a demo
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 works
  • Features
  • Demo

Resources

  • Solutions
  • Blog
  • Docs
  • Contact
  • Dashboard

Legal

  • Terms of Service
  • Privacy Policy
  • Security
  • DPA

© 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