⚛️ @xhub-chat/react
React integration for XHub Chat with hooks, context, and utilities for building chat UIs.
Overview
@xhub-chat/react provides React-specific tools to integrate XHub Chat into your React applications:
- ✅ React Hooks for common chat operations
- ✅ Context Provider for global client access
- ✅ Automatic state management and updates
- ✅ TypeScript support with full type inference
- ✅ Optimized re-renders with React best practices
Installation
pnpm add @xhub-chat/core @xhub-chat/react react
Setup
1. Wrap Your App with Provider
import { XHubChatProvider } from '@xhub-chat/react';
function App() {
return (
<XHubChatProvider
config={{
baseUrl: 'https://api.example.com',
accessToken: 'your-token',
}}
>
<YourChatApp />
</XHubChatProvider>
);
}
2. Use Hooks in Components
import { useRooms, useXHubChat } from '@xhub-chat/react';
function ChatRooms() {
const { rooms, loading } = useRooms();
if (loading) return <div>Loading...</div>;
return (
<div>
{rooms.map(room => (
<div key={room.roomId}>{room.name}</div>
))}
</div>
);
}
Core Hooks
useXHubChat
Access the XHub Chat client instance.
import { useXHubChat } from '@xhub-chat/react';
function MyComponent() {
const client = useXHubChat();
const sendMessage = async () => {
await client.sendTextMessage(roomId, 'Hello!');
};
return <button onClick={sendMessage}>Send</button>;
}
useRooms
Get all rooms the user has joined.
import { useRooms } from '@xhub-chat/react';
function RoomList() {
const { rooms, loading, error } = useRooms();
if (loading) return <div>Loading rooms...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{rooms.map(room => (
<li key={room.roomId}>
{room.name} ({room.getJoinedMemberCount()} members)
</li>
))}
</ul>
);
}
Return Value:
{
rooms: Room[]; // Array of Room objects
loading: boolean; // True while initial load
error: Error | null; // Error if loading failed
}
useTimeline
Get messages and events from a room's timeline.
import { useTimeline } from '@xhub-chat/react';
function MessageList({ roomId }: { roomId: string }) {
const {
events,
loading,
hasMore,
loadMore,
error,
} = useTimeline(roomId);
if (loading) return <div>Loading messages...</div>;
return (
<div>
{hasMore && (
<button onClick={loadMore}>Load More</button>
)}
{events.map(event => (
<div key={event.getId()}>
<strong>{event.getSender()}</strong>
<p>{event.getContent().body}</p>
</div>
))}
</div>
);
}
Return Value:
{
events: XHubChatEvent[]; // Array of events
loading: boolean; // Initial load state
hasMore: boolean; // Can load more messages
loadMore: () => void; // Load older messages
error: Error | null; // Error if any
}
useReactions
Manage reactions on messages.
import { useReactions } from '@xhub-chat/react';
function MessageWithReactions({ roomId, eventId }: Props) {
const {
reactions,
addReaction,
removeReaction,
loading,
} = useReactions(roomId, eventId);
return (
<div>
{reactions.map(reaction => (
<button
key={reaction.key}
onClick={() => removeReaction(reaction.key)}
>
{reaction.key} {reaction.count}
</button>
))}
<button onClick={() => addReaction('👍')}>
Add 👍
</button>
</div>
);
}
Return Value:
{
reactions: Array<{
key: string;
count: number;
userIds: string[];
}>;
addReaction: (emoji: string) => Promise<void>;
removeReaction: (emoji: string) => Promise<void>;
loading: boolean;
}
useThread
Get messages in a thread (replies to a message).
import { useThread } from '@xhub-chat/react';
function ThreadView({ roomId, eventId }: Props) {
const {
events,
loading,
sendReply,
} = useThread(roomId, eventId);
const handleReply = async (text: string) => {
await sendReply(text);
};
return (
<div>
<h3>Thread</h3>
{events.map(event => (
<div key={event.getId()}>{event.getContent().body}</div>
))}
<input onSubmit={handleReply} />
</div>
);
}
Utility Hooks
useEventEmitter
Listen to client events with automatic cleanup.
import { useEventEmitter } from '@xhub-chat/react';
function ConnectionStatus() {
const [status, setStatus] = useState('disconnected');
const client = useXHubChat();
useEventEmitter(client, 'sync', (state) => {
setStatus(state);
});
return <div>Status: {status}</div>;
}
useTypingIndicator
Show typing indicators for a room.
import { useTypingIndicator } from '@xhub-chat/react';
function TypingIndicator({ roomId }: { roomId: string }) {
const { typingUsers, sendTyping } = useTypingIndicator(roomId);
const handleKeyPress = () => {
sendTyping();
};
if (typingUsers.length === 0) return null;
return (
<div>
{typingUsers.map(u => u.name).join(', ')} is typing...
</div>
);
}
Advanced Usage
Custom Event Handling
import { useEffect } from 'react';
import { useXHubChat } from '@xhub-chat/react';
function CustomEventHandler() {
const client = useXHubChat();
useEffect(() => {
const handler = (event, room) => {
if (event.getType() === 'com.example.custom') {
console.log('Custom event received');
}
};
client.on('Room.timeline', handler);
return () => client.off('Room.timeline', handler);
}, [client]);
return null;
}
Optimistic Updates
function MessageSender({ roomId }: { roomId: string }) {
const client = useXHubChat();
const [messages, setMessages] = useState<any[]>([]);
const sendMessage = async (text: string) => {
// Add optimistic message
const optimisticMsg = {
id: Date.now(),
text,
pending: true,
};
setMessages(prev => [...prev, optimisticMsg]);
try {
await client.sendTextMessage(roomId, text);
// Update with real message
setMessages(prev =>
prev.filter(m => m.id !== optimisticMsg.id)
);
} catch (error) {
// Mark as failed
setMessages(prev =>
prev.map(m =>
m.id === optimisticMsg.id
? { ...m, failed: true }
: m
)
);
}
};
return <MessageInput onSend={sendMessage} />;
}
Pagination with Infinite Scroll
import { useTimeline } from '@xhub-chat/react';
import InfiniteScroll from 'react-infinite-scroll-component';
function InfiniteMessageList({ roomId }: { roomId: string }) {
const { events, hasMore, loadMore } = useTimeline(roomId);
return (
<InfiniteScroll
dataLength={events.length}
next={loadMore}
hasMore={hasMore}
loader={<div>Loading...</div>}
scrollableTarget="message-list"
>
{events.map(event => (
<Message key={event.getId()} event={event} />
))}
</InfiniteScroll>
);
}
TypeScript Support
Full TypeScript support with type inference:
import type {
Room,
XHubChatEvent,
XHubChatClient,
} from '@xhub-chat/react';
const client: XHubChatClient = useXHubChat();
const rooms: Room[] = useRooms().rooms;
const events: XHubChatEvent[] = useTimeline(roomId).events;
Performance Optimization
Memoization
import { memo, useMemo } from 'react';
import { useTimeline } from '@xhub-chat/react';
const MessageList = memo(({ roomId }: { roomId: string }) => {
const { events } = useTimeline(roomId);
const messageComponents = useMemo(
() => events.map(e => <Message key={e.getId()} event={e} />),
[events]
);
return <div>{messageComponents}</div>;
});
Selective Re-renders
import { useRooms } from '@xhub-chat/react';
function RoomCount() {
const { rooms } = useRooms();
// Only re-render when count changes
const roomCount = useMemo(() => rooms.length, [rooms.length]);
return <div>{roomCount} rooms</div>;
}
Best Practices
- Use Context Provider: Always wrap your app with
XHubChatProvider - Clean Up Effects: Remove event listeners in cleanup functions
- Handle Loading States: Show loading indicators while data loads
- Handle Errors: Display error messages to users
- Optimize Re-renders: Use
memo,useMemo, anduseCallback - Type Everything: Leverage TypeScript for type safety
Common Patterns
Room Switcher
function RoomSwitcher() {
const { rooms } = useRooms();
const [activeRoomId, setActiveRoomId] = useState<string | null>(null);
return (
<div>
<aside>
{rooms.map(room => (
<button
key={room.roomId}
onClick={() => setActiveRoomId(room.roomId)}
>
{room.name}
</button>
))}
</aside>
<main>
{activeRoomId && <ChatRoom roomId={activeRoomId} />}
</main>
</div>
);
}
Search Messages
function MessageSearch({ roomId }: { roomId: string }) {
const { events } = useTimeline(roomId);
const [query, setQuery] = useState('');
const filteredEvents = useMemo(() => {
if (!query) return events;
return events.filter(e =>
e.getContent().body?.toLowerCase().includes(query.toLowerCase())
);
}, [events, query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search messages..."
/>
{filteredEvents.map(e => <Message key={e.getId()} event={e} />)}
</div>
);
}
Next Steps
- 📘 API Reference - Complete hooks documentation
- 💡 Guides - React integration guide
- 💻 Examples - Full React examples