Overview
useChat
enables real-time messaging between users in your collaborative application. It provides a complete chat system with message history, automatic user identification, and timestamp tracking. Perfect for building chat interfaces, comments systems, and any collaborative communication feature.
Perfect for: Real-time chat applications, team communication, comment systems, collaborative feedback, customer support chat, and any feature requiring user-to-user messaging.
Basic Usage
Copy
Ask AI
import { useEffect, useRef, useState } from 'react'
import { useChat } from 'react-together'
function ChatComponent() {
const { messages, sendMessage } = useChat('my-chat')
const [message, setMessage] = useState('')
const messagesRef = useRef<HTMLDivElement>(null)
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
if (messagesRef.current) {
messagesRef.current.scrollTop = messagesRef.current.scrollHeight
}
}, [messages])
const handleSendMessage = () => {
if (message.trim()) {
sendMessage(message.trim())
setMessage('')
}
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSendMessage()
}
}
return (
<div className="flex flex-col h-96 border rounded-lg">
{/* Messages Container */}
<div
ref={messagesRef}
className="flex-1 overflow-y-auto p-4 space-y-2"
>
{messages.map(({ id, senderId, message, sentAt }) => (
<div key={id} className="text-sm">
<span className="text-gray-500">
[{new Date(sentAt).toLocaleTimeString()}]
</span>{' '}
<strong className="text-blue-600">{senderId}</strong>: {message}
</div>
))}
</div>
{/* Input Container */}
<div className="p-4 border-t flex gap-2">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type a message..."
className="flex-1 border border-gray-300 rounded px-3 py-2"
/>
<button
onClick={handleSendMessage}
disabled={!message.trim()}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
Send
</button>
</div>
</div>
)
}
Signature
Copy
Ask AI
useChat(rtKey: string): {
messages: ChatMessage[]
sendMessage: (message: string) => void
}
Parameters
Unique identifier for this chat instance. Use different keys for separate chat rooms or conversations.
Return Value
Array of all messages in the chat, ordered chronologically
Function to send a new message to the chat
ChatMessage Interface
Unique identifier for the message
User ID of the user who sent the message
The text content of the message
Timestamp (in milliseconds) when the message was sent
Examples
Modern Chat Interface
Copy
Ask AI
import { useChat, useNicknames, useConnectedUsers } from 'react-together'
import { useState, useRef, useEffect } from 'react'
function ModernChat() {
const { messages, sendMessage } = useChat('team-chat')
const [, , allNicknames] = useNicknames()
const [input, setInput] = useState('')
const connectedUsers = useConnectedUsers()
const messagesEndRef = useRef<HTMLDivElement>(null)
const currentUser = connectedUsers.find(u => u.isYou)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim()) {
sendMessage(input.trim())
setInput('')
}
}
const formatTime = (timestamp: number) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})
}
const isConsecutiveMessage = (currentMsg: any, prevMsg: any) => {
return prevMsg &&
prevMsg.senderId === currentMsg.senderId &&
(currentMsg.sentAt - prevMsg.sentAt) < 60000 // Within 1 minute
}
return (
<div className="bg-white rounded-lg shadow-lg flex flex-col h-[500px]">
{/* Header */}
<div className="bg-gray-50 px-4 py-3 border-b rounded-t-lg">
<h3 className="font-semibold text-gray-800">Team Chat</h3>
<p className="text-sm text-gray-600">
{connectedUsers.length} user(s) online
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-1">
{messages.map((msg, index) => {
const prevMsg = messages[index - 1]
const isOwn = msg.senderId === currentUser?.userId
const isConsecutive = isConsecutiveMessage(msg, prevMsg)
const senderName = allNicknames[msg.senderId] || msg.senderId
return (
<div key={msg.id}>
<div className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}>
<div className={`max-w-xs lg:max-w-md ${isOwn ? 'order-1' : ''}`}>
{!isConsecutive && (
<div className={`text-xs text-gray-500 mb-1 ${isOwn ? 'text-right' : 'text-left'}`}>
{isOwn ? 'You' : senderName} • {formatTime(msg.sentAt)}
</div>
)}
<div
className={`px-3 py-2 rounded-lg ${
isOwn
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-800'
} ${!isConsecutive ? 'rounded-tl-lg' : isOwn ? 'rounded-tr-md' : 'rounded-tl-md'}`}
>
{msg.message}
</div>
</div>
</div>
</div>
)
})}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t bg-gray-50">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type your message..."
className="flex-1 border border-gray-300 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
type="submit"
disabled={!input.trim()}
className="bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white px-6 py-2 rounded-full font-medium transition-colors"
>
Send
</button>
</div>
</form>
</div>
)
}
Multi-Room Chat System
Copy
Ask AI
import { useChat, useStateTogether, useNicknames } from 'react-together'
import { useState } from 'react'
interface ChatRoom {
id: string
name: string
description: string
}
function MultiRoomChat() {
const [rooms] = useStateTogether<ChatRoom[]>('chat-rooms', [
{ id: 'general', name: 'General', description: 'General discussion' },
{ id: 'dev', name: 'Development', description: 'Development topics' },
{ id: 'design', name: 'Design', description: 'Design discussions' }
])
const [activeRoom, setActiveRoom] = useState('general')
const { messages, sendMessage } = useChat(`room-${activeRoom}`)
const [, , allNicknames] = useNicknames()
const [input, setInput] = useState('')
const handleSendMessage = () => {
if (input.trim()) {
sendMessage(input.trim())
setInput('')
}
}
const currentRoom = rooms.find(r => r.id === activeRoom)
return (
<div className="flex h-[600px] bg-white rounded-lg shadow-lg overflow-hidden">
{/* Sidebar */}
<div className="w-64 bg-gray-100 border-r">
<div className="p-4 border-b">
<h3 className="font-semibold text-gray-800">Chat Rooms</h3>
</div>
<div className="p-2">
{rooms.map((room) => (
<button
key={room.id}
onClick={() => setActiveRoom(room.id)}
className={`w-full text-left p-3 rounded-lg mb-1 transition-colors ${
activeRoom === room.id
? 'bg-blue-500 text-white'
: 'hover:bg-gray-200 text-gray-700'
}`}
>
<div className="font-medium"># {room.name}</div>
<div className="text-sm opacity-75">{room.description}</div>
</button>
))}
</div>
</div>
{/* Chat Area */}
<div className="flex-1 flex flex-col">
{/* Room Header */}
<div className="bg-white border-b p-4">
<h2 className="font-semibold text-lg"># {currentRoom?.name}</h2>
<p className="text-gray-600 text-sm">{currentRoom?.description}</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4">
{messages.length === 0 ? (
<div className="text-center text-gray-500 py-8">
<p>No messages yet in #{currentRoom?.name}</p>
<p className="text-sm">Be the first to start the conversation!</p>
</div>
) : (
<div className="space-y-3">
{messages.map((msg) => (
<div key={msg.id} className="flex items-start space-x-3">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-semibold">
{(allNicknames[msg.senderId] || msg.senderId).charAt(0).toUpperCase()}
</div>
<div className="flex-1">
<div className="flex items-baseline space-x-2">
<span className="font-semibold text-gray-900">
{allNicknames[msg.senderId] || msg.senderId}
</span>
<span className="text-xs text-gray-500">
{new Date(msg.sentAt).toLocaleString()}
</span>
</div>
<p className="text-gray-700 mt-1">{msg.message}</p>
</div>
</div>
))}
</div>
)}
</div>
{/* Input */}
<div className="p-4 border-t bg-gray-50">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder={`Message #${currentRoom?.name}`}
className="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleSendMessage}
disabled={!input.trim()}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
Send
</button>
</div>
</div>
</div>
</div>
)
}
Chat with Rich Features
Copy
Ask AI
import { useChat, useNicknames, useConnectedUsers, useStateTogether } from 'react-together'
import { useState, useRef, useEffect } from 'react'
interface Reaction {
messageId: number
userId: string
emoji: string
}
function RichChat() {
const { messages, sendMessage } = useChat('rich-chat')
const [, , allNicknames] = useNicknames()
const [reactions, setReactions] = useStateTogether<Reaction[]>('chat-reactions', [])
const [typingUsers, setTypingUsers] = useStateTogether<string[]>('typing-users', [])
const [input, setInput] = useState('')
const [showEmojiPicker, setShowEmojiPicker] = useState<number | null>(null)
const connectedUsers = useConnectedUsers()
const typingTimeoutRef = useRef<NodeJS.Timeout>()
const currentUser = connectedUsers.find(u => u.isYou)
const emojis = ['👍', '❤️', '😂', '😮', '😢', '😡']
// Handle typing indicators
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
if (currentUser && !typingUsers.includes(currentUser.userId)) {
setTypingUsers(prev => [...prev, currentUser.userId])
}
clearTimeout(typingTimeoutRef.current)
typingTimeoutRef.current = setTimeout(() => {
if (currentUser) {
setTypingUsers(prev => prev.filter(id => id !== currentUser.userId))
}
}, 1000)
}
const handleSendMessage = () => {
if (input.trim() && currentUser) {
sendMessage(input.trim())
setInput('')
setTypingUsers(prev => prev.filter(id => id !== currentUser.userId))
}
}
const addReaction = (messageId: number, emoji: string) => {
if (!currentUser) return
const existingReaction = reactions.find(
r => r.messageId === messageId && r.userId === currentUser.userId && r.emoji === emoji
)
if (existingReaction) {
// Remove reaction
setReactions(prev => prev.filter(r => r !== existingReaction))
} else {
// Add reaction
setReactions(prev => [...prev, {
messageId,
userId: currentUser.userId,
emoji
}])
}
setShowEmojiPicker(null)
}
const getReactionsForMessage = (messageId: number) => {
const messageReactions = reactions.filter(r => r.messageId === messageId)
const grouped = messageReactions.reduce((acc, reaction) => {
if (!acc[reaction.emoji]) {
acc[reaction.emoji] = []
}
acc[reaction.emoji].push(reaction.userId)
return acc
}, {} as Record<string, string[]>)
return grouped
}
const typingUserNames = typingUsers
.filter(userId => userId !== currentUser?.userId)
.map(userId => allNicknames[userId] || userId)
return (
<div className="bg-white rounded-lg shadow-lg flex flex-col h-[600px]">
{/* Header */}
<div className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-4 py-3 rounded-t-lg">
<h3 className="font-semibold">Rich Chat</h3>
<p className="text-sm opacity-90">
{connectedUsers.length} user(s) online
</p>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => {
const isOwn = msg.senderId === currentUser?.userId
const senderName = allNicknames[msg.senderId] || msg.senderId
const messageReactions = getReactionsForMessage(msg.id)
return (
<div key={msg.id} className={`flex ${isOwn ? 'justify-end' : 'justify-start'}`}>
<div className="max-w-md">
{!isOwn && (
<div className="text-sm font-medium text-gray-600 mb-1">
{senderName}
</div>
)}
<div className="relative group">
<div
className={`px-4 py-2 rounded-lg ${
isOwn
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-800'
}`}
>
{msg.message}
</div>
{/* Reaction Button */}
<button
onClick={() => setShowEmojiPicker(showEmojiPicker === msg.id ? null : msg.id)}
className="absolute -right-2 -top-2 bg-gray-200 hover:bg-gray-300 rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
😊
</button>
{/* Emoji Picker */}
{showEmojiPicker === msg.id && (
<div className="absolute top-0 right-0 bg-white border rounded-lg shadow-lg p-2 flex gap-1 z-10">
{emojis.map((emoji) => (
<button
key={emoji}
onClick={() => addReaction(msg.id, emoji)}
className="hover:bg-gray-100 rounded p-1"
>
{emoji}
</button>
))}
</div>
)}
</div>
{/* Reactions */}
{Object.keys(messageReactions).length > 0 && (
<div className="flex gap-1 mt-2">
{Object.entries(messageReactions).map(([emoji, userIds]) => (
<button
key={emoji}
onClick={() => addReaction(msg.id, emoji)}
className={`text-xs px-2 py-1 rounded-full border ${
currentUser && userIds.includes(currentUser.userId)
? 'bg-blue-100 border-blue-300'
: 'bg-gray-100 border-gray-300'
} hover:bg-gray-200`}
title={userIds.map(id => allNicknames[id] || id).join(', ')}
>
{emoji} {userIds.length}
</button>
))}
</div>
)}
<div className="text-xs text-gray-500 mt-1">
{new Date(msg.sentAt).toLocaleTimeString()}
</div>
</div>
</div>
)
})}
</div>
{/* Typing Indicator */}
{typingUserNames.length > 0 && (
<div className="px-4 py-2 text-sm text-gray-500 italic">
{typingUserNames.join(', ')} {typingUserNames.length === 1 ? 'is' : 'are'} typing...
</div>
)}
{/* Input */}
<div className="p-4 border-t">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={handleInputChange}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
placeholder="Type a message..."
className="flex-1 border border-gray-300 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleSendMessage}
disabled={!input.trim()}
className="bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-6 py-2 rounded-full"
>
Send
</button>
</div>
</div>
</div>
)
}
Minimal Comment System
Copy
Ask AI
import { useChat, useNicknames } from 'react-together'
import { useState } from 'react'
interface CommentSystemProps {
postId: string
title: string
}
function CommentSystem({ postId, title }: CommentSystemProps) {
const { messages: comments, sendMessage: addComment } = useChat(`comments-${postId}`)
const [, , allNicknames] = useNicknames()
const [newComment, setNewComment] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (newComment.trim()) {
addComment(newComment.trim())
setNewComment('')
}
}
return (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Comments on "{title}"</h3>
{/* Comments List */}
<div className="space-y-4 mb-6">
{comments.length === 0 ? (
<p className="text-gray-500 text-center py-4">
No comments yet. Be the first to comment!
</p>
) : (
comments.map((comment) => (
<div key={comment.id} className="border-l-4 border-blue-500 pl-4">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-gray-900">
{allNicknames[comment.senderId] || comment.senderId}
</span>
<span className="text-sm text-gray-500">
{new Date(comment.sentAt).toLocaleDateString()}
</span>
</div>
<p className="text-gray-700">{comment.message}</p>
</div>
))
)}
</div>
{/* Add Comment Form */}
<form onSubmit={handleSubmit} className="space-y-3">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
rows={3}
className="w-full border border-gray-300 rounded px-3 py-2 resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={!newComment.trim()}
className="bg-blue-500 hover:bg-blue-600 disabled:opacity-50 text-white px-4 py-2 rounded"
>
Post Comment
</button>
</form>
</div>
)
}
// Usage
function BlogPost() {
return (
<div className="max-w-2xl mx-auto space-y-8">
<article className="bg-white rounded-lg shadow p-6">
<h1 className="text-2xl font-bold mb-4">My Blog Post</h1>
<p>This is the content of my blog post...</p>
</article>
<CommentSystem
postId="blog-post-1"
title="My Blog Post"
/>
</div>
)
}
Best Practices
Performance Considerations
- Unique Keys: Use descriptive, unique
rtKey
values for different chat instances - Message Limits: Consider implementing message history limits for long-running chats
- Auto-scroll: Implement smooth auto-scrolling for better user experience
User Experience
- Message Formatting: Support line breaks and basic formatting in messages
- Timestamps: Always display timestamps for better context
- User Identification: Combine with
useNicknames
for better user identification - Typing Indicators: Add typing indicators for real-time feedback
Security & Moderation
Copy
Ask AI
// Input validation
const validateMessage = (message: string) => {
return message.trim().length > 0 && message.length <= 500
}
// Message filtering (implement on your backend)
const filterMessage = (message: string) => {
// Remove or replace inappropriate content
return message.replace(/spam|abuse/gi, '***')
}
Related Hooks
useNicknames
- Display user-friendly names in chatuseConnectedUsers
- Show online usersuseStateTogether
- Add additional chat features
TypeScript Support
The hook is fully typed with comprehensive TypeScript support:Copy
Ask AI
interface ChatMessage {
id: number
senderId: string
message: string
sentAt: number
}
const {
messages, // ChatMessage[]
sendMessage // (message: string) => void
} = useChat('my-chat')