React Integration
Use ChatSDK with React using our hooks and components.
Setup
npm install @chatsdk/core @chatsdk/react
Provider Setup
Wrap your app with the ChatProvider:
import { ChatProvider, ChatClient } from '@chatsdk/react';
const client = new ChatClient({
apiKey: 'your-api-key',
apiUrl: 'https://api.your-server.com',
});
function App() {
return (
<ChatProvider client={client}>
<YourApp />
</ChatProvider>
);
}
Authentication
Connect the user after login:
import { useChatClient } from '@chatsdk/react';
function AuthWrapper({ children }) {
const client = useChatClient();
const { user: authUser } = useAuth(); // Your auth context
useEffect(() => {
if (authUser) {
const connect = async () => {
const token = await getChatToken(); // From your backend
await client.connectUser(
{ id: authUser.id, name: authUser.name },
token
);
};
connect();
return () => client.disconnect();
}
}, [authUser, client]);
return children;
}
Hooks
useChannels
import { useChannels } from '@chatsdk/react';
function ChannelList() {
const { channels, loading, error, refresh } = useChannels();
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<ul>
{channels.map(channel => (
<li key={channel.id}>
{channel.name}
{channel.unreadCount > 0 && (
<Badge count={channel.unreadCount} />
)}
</li>
))}
</ul>
);
}
useMessages
import { useMessages } from '@chatsdk/react';
function MessageList({ channelId }) {
const { messages, loading, hasMore, loadMore } = useMessages(channelId);
return (
<div>
{hasMore && (
<button onClick={loadMore}>Load older messages</button>
)}
{messages.map(message => (
<MessageBubble key={message.id} message={message} />
))}
</div>
);
}
useReactions
import { useReactions, QUICK_REACTIONS } from '@chatsdk/react';
function MessageReactions({ channelId, message }) {
const { addReaction, removeReaction, toggleReaction } = useReactions(channelId);
return (
<div className="reactions">
{/* Show existing reactions */}
{message.reactions?.map(reaction => (
<button
key={reaction.type}
onClick={() => toggleReaction(message.id, reaction.type, reaction.own)}
className={reaction.own ? 'active' : ''}
>
{reaction.type} {reaction.count}
</button>
))}
{/* Quick reaction picker */}
<div className="reaction-picker">
{QUICK_REACTIONS.map(emoji => (
<button
key={emoji}
onClick={() => addReaction(message.id, emoji)}
>
{emoji}
</button>
))}
</div>
</div>
);
}
useTypingIndicator
import { useTypingIndicator } from '@chatsdk/react';
function TypingIndicator({ channelId }) {
const { typingUsers } = useTypingIndicator(channelId);
if (typingUsers.length === 0) return null;
if (typingUsers.length === 1) {
return <span>{typingUsers[0].name} is typing...</span>;
}
return <span>Several people are typing...</span>;
}
useMentions
import { useMentions, useMentionSearch, highlightMentions } from '@chatsdk/react';
// Display mentions badge
function MentionsBadge() {
const { unreadCount } = useMentions();
return unreadCount > 0 ? <Badge count={unreadCount} /> : null;
}
// Mention autocomplete in composer
function MentionAutocomplete({ channelId, onSelect }) {
const { query, setQuery, users, loading } = useMentionSearch({ channelId });
return (
<div className="mention-autocomplete">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search users..."
/>
{users.map(user => (
<button key={user.id} onClick={() => onSelect(user)}>
<Avatar user={user} />
{user.name}
</button>
))}
</div>
);
}
// Render message with highlighted mentions
function MessageText({ text, currentUserId }) {
const parts = highlightMentions(text, currentUserId);
return (
<p>
{parts.map((part, i) => (
part.type === 'mention' ? (
<span
key={i}
className={`mention ${part.isCurrentUser ? 'mention-me' : ''}`}
>
@{part.content}
</span>
) : (
<span key={i}>{part.content}</span>
)
))}
</p>
);
}
useReadReceipts
import { useReadReceipts } from '@chatsdk/react';
function MessageStatus({ channelId, messageId }) {
const { getReceipts, formatReadReceipt } = useReadReceipts(channelId);
const receipts = getReceipts(messageId);
return (
<span className="read-status">
{formatReadReceipt(receipts)}
</span>
);
}
usePresence
import { usePresence } from '@chatsdk/react';
function UserStatus({ userId }) {
const { isOnline, lastSeen } = usePresence(userId);
if (isOnline) {
return <span className="status online">Online</span>;
}
return (
<span className="status offline">
Last seen {formatDistanceToNow(lastSeen)} ago
</span>
);
}
Message Composer
Complete message composer example:
import { useState, useRef } from 'react';
import { useChatClient, useTypingIndicator, useFileUpload } from '@chatsdk/react';
function MessageComposer({ channelId }) {
const [text, setText] = useState('');
const [attachments, setAttachments] = useState([]);
const client = useChatClient();
const { startTyping, stopTyping } = useTypingIndicator(channelId);
const { upload, uploading, progress } = useFileUpload();
const typingTimeout = useRef(null);
const handleTextChange = (e) => {
setText(e.target.value);
// Debounced typing indicator
startTyping();
clearTimeout(typingTimeout.current);
typingTimeout.current = setTimeout(stopTyping, 2000);
};
const handleFileSelect = async (files) => {
const uploaded = await Promise.all(
files.map(file => upload(file, channelId))
);
setAttachments([...attachments, ...uploaded]);
};
const handleSend = async () => {
if (!text.trim() && attachments.length === 0) return;
await client.sendMessage(channelId, {
text: text.trim(),
attachments,
});
setText('');
setAttachments([]);
stopTyping();
};
return (
<div className="composer">
{/* Attachment previews */}
{attachments.length > 0 && (
<div className="attachments-preview">
{attachments.map((att, i) => (
<AttachmentPreview
key={i}
attachment={att}
onRemove={() => setAttachments(attachments.filter((_, j) => j !== i))}
/>
))}
</div>
)}
{uploading && <ProgressBar value={progress} />}
<div className="input-row">
<input
type="file"
multiple
onChange={(e) => handleFileSelect([...e.target.files])}
/>
<input
type="text"
value={text}
onChange={handleTextChange}
placeholder="Type a message..."
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend} disabled={!text.trim() && attachments.length === 0}>
Send
</button>
</div>
</div>
);
}
Complete Chat Component
import {
useChannels,
useMessages,
useTypingIndicator,
ChatProvider,
} from '@chatsdk/react';
function Chat() {
const [selectedChannel, setSelectedChannel] = useState(null);
return (
<div className="chat-container">
<ChannelSidebar
selectedId={selectedChannel?.id}
onSelect={setSelectedChannel}
/>
{selectedChannel && (
<ChatRoom channel={selectedChannel} />
)}
</div>
);
}
function ChannelSidebar({ selectedId, onSelect }) {
const { channels, loading } = useChannels();
if (loading) return <Spinner />;
return (
<aside className="channel-sidebar">
{channels.map(channel => (
<button
key={channel.id}
className={channel.id === selectedId ? 'active' : ''}
onClick={() => onSelect(channel)}
>
<span className="name">{channel.name}</span>
{channel.unreadCount > 0 && (
<span className="unread">{channel.unreadCount}</span>
)}
</button>
))}
</aside>
);
}
function ChatRoom({ channel }) {
const { messages, loading, hasMore, loadMore } = useMessages(channel.id);
const { typingUsers } = useTypingIndicator(channel.id);
return (
<main className="chat-room">
<header>{channel.name}</header>
<div className="messages">
{hasMore && <button onClick={loadMore}>Load more</button>}
{messages.map(msg => (
<MessageBubble key={msg.id} message={msg} />
))}
<TypingIndicator users={typingUsers} />
</div>
<MessageComposer channelId={channel.id} />
</main>
);
}
Next Steps
- API Reference - Full React hooks documentation
- Examples - Complete example apps