🪝 React Hooks API
Complete reference for all React hooks provided by @xhub-chat/react.
Core Hooks
useXHubChat
Access the XHub Chat client from context.
Type Signature:
function useXHubChat(): XHubChatContextValue
interface XHubChatContextValue {
client: XHubChatClient;
rooms: Room[];
getRoom: (roomId: string) => Room | null;
}
Usage:
import { useXHubChat } from '@xhub-chat/react';
function MyComponent() {
const { client, rooms, getRoom } = useXHubChat();
const handleSend = async () => {
await client.sendTextMessage(roomId, 'Hello!');
};
return <div>Total rooms: {rooms.length}</div>;
}
Returns:
client- XHub Chat client instancerooms- Array of all rooms (auto-updates)getRoom- Function to get room by ID
Throws:
Error- If used outsideXHubChatProvider
Example - Send Message:
function SendButton({ roomId }: { roomId: string }) {
const { client } = useXHubChat();
const send = async () => {
try {
await client.sendTextMessage(roomId, 'Hello World!');
} catch (error) {
console.error('Failed to send:', error);
}
};
return <button onClick={send}>Send</button>;
}
useRooms
Access all rooms with pagination support.
Type Signature:
function useRooms(): IUserRooms
interface IUserRooms {
rooms: Room[];
canPaginate: boolean;
error: Error | null;
fetching?: boolean;
paginate: (limit: number) => Promise<void>;
getRoomById: (roomId: string) => Room | null;
}
Usage:
import { useRooms } from '@xhub-chat/react';
function RoomList() {
const { rooms, canPaginate, paginate, fetching } = useRooms();
return (
<div>
{rooms.map(room => (
<div key={room.roomId}>{room.name}</div>
))}
{canPaginate && (
<button onClick={() => paginate(20)} disabled={fetching}>
{fetching ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
Returns:
rooms- Array of all rooms (auto-updates)canPaginate- Whether more rooms can be loadederror- Any pagination errorfetching- Whether currently loadingpaginate(limit)- Function to load more roomsgetRoomById(id)- Get specific room
Example - Infinite Scroll:
function InfiniteRoomList() {
const { rooms, canPaginate, paginate } = useRooms();
const observerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!canPaginate) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
paginate(20);
}
},
{ threshold: 0.1 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, [canPaginate, paginate]);
return (
<div>
{rooms.map(room => <RoomItem key={room.roomId} room={room} />)}
<div ref={observerRef} />
</div>
);
}
useTimeline
Access and manage a room's timeline (messages).
Type Signature:
function useTimeline(props: IUseTimelineProps): IUseTimeline
interface IUseTimelineProps {
roomId: string;
timelineSet?: EventTimelineSet;
}
interface IUseTimeline {
events: XHubChatEvent[];
room: Room | null;
isLoading: boolean;
error: string | null;
// Pagination
canPaginateForwards: boolean;
canPaginateBackwards: boolean;
isPaginatingForwards: boolean;
isPaginatingBackwards: boolean;
paginate: (direction: 'f' | 'b', limit?: number) => Promise<void>;
// Messaging
sendTextMessage: (text: string) => Promise<void>;
sendMessage: (content: any) => Promise<void>;
resendEvent: (eventId: string) => Promise<void>;
cancelPendingEvent: (eventId: string) => void;
// Reactions
addReaction: (eventId: string, emoji: string) => Promise<void>;
removeReaction: (eventId: string, emoji: string) => Promise<void>;
getReactions: (eventId: string) => Relations | null;
// Replies & Threads
replyToEvent: (eventId: string, text: string) => Promise<void>;
getThread: (eventId: string) => Thread | undefined;
// Read receipts
sendReadReceipt: (eventId: string) => Promise<void>;
}
Usage:
import { useTimeline } from '@xhub-chat/react';
function ChatRoom({ roomId }: { roomId: string }) {
const {
events,
isLoading,
sendTextMessage,
canPaginateBackwards,
paginate,
} = useTimeline({ roomId });
const [message, setMessage] = useState('');
if (isLoading) return <div>Loading...</div>;
return (
<div>
{canPaginateBackwards && (
<button onClick={() => paginate('b', 30)}>
Load Older Messages
</button>
)}
{events.map(event => (
<Message key={event.getId()} event={event} />
))}
<input
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button onClick={() => {
sendTextMessage(message);
setMessage('');
}}>
Send
</button>
</div>
);
}
Returns:
Timeline Data
events- Array of timeline events (auto-updates)room- Room objectisLoading- Initial loading stateerror- Any error message
Pagination
canPaginateForwards- Can load newer messagescanPaginateBackwards- Can load older messagesisPaginatingForwards- Currently loading newerisPaginatingBackwards- Currently loading olderpaginate(direction, limit)- Load more messagesdirection:'f'(forwards) or'b'(backwards)limit: Number of messages (default: 30)
Messaging
sendTextMessage(text)- Send text messagesendMessage(content)- Send custom contentresendEvent(eventId)- Retry failed messagecancelPendingEvent(eventId)- Cancel pending message
Reactions
addReaction(eventId, emoji)- Add reactionremoveReaction(eventId, emoji)- Remove reactiongetReactions(eventId)- Get all reactions
Threading
replyToEvent(eventId, text)- Reply to messagegetThread(eventId)- Get thread data
Read Receipts
sendReadReceipt(eventId)- Mark as read
Example - Complete Chat:
function CompleteChat({ roomId }: { roomId: string }) {
const {
events,
sendTextMessage,
addReaction,
replyToEvent,
paginate,
canPaginateBackwards,
} = useTimeline({ roomId });
const [message, setMessage] = useState('');
const [replyTo, setReplyTo] = useState<string | null>(null);
const handleSend = async () => {
if (!message.trim()) return;
if (replyTo) {
await replyToEvent(replyTo, message);
setReplyTo(null);
} else {
await sendTextMessage(message);
}
setMessage('');
};
return (
<div className="chat">
<div className="messages">
{canPaginateBackwards && (
<button onClick={() => paginate('b')}>
Load Older
</button>
)}
{events.map(event => (
<div key={event.getId()} className="message">
<div className="content">
{event.getContent().body}
</div>
<div className="actions">
<button onClick={() => addReaction(event.getId(), '👍')}>
👍
</button>
<button onClick={() => setReplyTo(event.getId())}>
Reply
</button>
</div>
</div>
))}
</div>
{replyTo && (
<div className="reply-bar">
Replying to message
<button onClick={() => setReplyTo(null)}>Cancel</button>
</div>
)}
<div className="input">
<input
value={message}
onChange={e => setMessage(e.target.value)}
onKeyPress={e => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}
Example - Reactions:
function MessageWithReactions({ event }: { event: XHubChatEvent }) {
const { getReactions, addReaction, removeReaction } = useTimeline({
roomId: event.getRoomId()!,
});
const reactions = getReactions(event.getId());
const reactionGroups = reactions?.getSortedAnnotationsByKey() || [];
return (
<div>
<div>{event.getContent().body}</div>
<div className="reactions">
{reactionGroups.map(([emoji, events]) => (
<button
key={emoji}
onClick={() => {
const myReaction = events.find(
e => e.getSender() === client.getUserId()
);
if (myReaction) {
removeReaction(event.getId(), emoji);
} else {
addReaction(event.getId(), emoji);
}
}}
>
{emoji} {events.size}
</button>
))}
<button onClick={() => addReaction(event.getId(), '❤️')}>
Add Reaction
</button>
</div>
</div>
);
}
Utility Hooks
Custom Hooks
You can create custom hooks by combining the core hooks:
Example - useUnreadCount:
import { useEffect, useState } from 'react';
import { useXHubChat } from '@xhub-chat/react';
import { ClientEvent } from '@xhub-chat/core';
export function useUnreadCount(roomId: string): number {
const { client, getRoom } = useXHubChat();
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const room = getRoom(roomId);
if (!room) return;
const updateCount = () => {
setUnreadCount(room.getUnreadNotificationCount('total') || 0);
};
updateCount();
client.on(ClientEvent.RoomTimeline, updateCount);
return () => {
client.off(ClientEvent.RoomTimeline, updateCount);
};
}, [client, roomId, getRoom]);
return unreadCount;
}
Example - useTypingIndicator:
import { useEffect, useState } from 'react';
import { useXHubChat } from '@xhub-chat/react';
import { RoomEvent } from '@xhub-chat/core';
export function useTypingIndicator(roomId: string): string[] {
const { getRoom } = useXHubChat();
const [typing, setTyping] = useState<string[]>([]);
useEffect(() => {
const room = getRoom(roomId);
if (!room) return;
const onTyping = () => {
const members = room.getMembersWithMembership('join')
.filter(m => m.typing && m.userId !== room.client.getUserId())
.map(m => m.name);
setTyping(members);
};
room.on(RoomEvent.Typing, onTyping);
return () => {
room.off(RoomEvent.Typing, onTyping);
};
}, [roomId, getRoom]);
return typing;
}
Example - useRoomMember:
import { useEffect, useState } from 'react';
import { useXHubChat } from '@xhub-chat/react';
import { RoomMember, RoomEvent } from '@xhub-chat/core';
export function useRoomMember(
roomId: string,
userId: string
): RoomMember | null {
const { getRoom } = useXHubChat();
const [member, setMember] = useState<RoomMember | null>(null);
useEffect(() => {
const room = getRoom(roomId);
if (!room) return;
const updateMember = () => {
setMember(room.getMember(userId));
};
updateMember();
room.on(RoomEvent.MembershipChanged, updateMember);
return () => {
room.off(RoomEvent.MembershipChanged, updateMember);
};
}, [roomId, userId, getRoom]);
return member;
}
Best Practices
1. Always Use Within Provider
// ✅ Good
function App() {
return (
<XHubChatProvider config={config}>
<ChatComponent />
</XHubChatProvider>
);
}
// ❌ Bad - will throw error
function App() {
return <ChatComponent />; // No provider!
}
2. Memoize Callbacks
// ✅ Good
const handleSend = useCallback(async () => {
await sendTextMessage(message);
}, [sendTextMessage, message]);
// ❌ Bad - creates new function every render
const handleSend = async () => {
await sendTextMessage(message);
};
3. Clean Up Event Listeners
// ✅ Good
useEffect(() => {
const onEvent = () => { /* ... */ };
client.on(ClientEvent.Room, onEvent);
return () => {
client.off(ClientEvent.Room, onEvent);
};
}, [client]);
4. Handle Loading States
// ✅ Good
const { events, isLoading } = useTimeline({ roomId });
if (isLoading) {
return <Spinner />;
}
// ❌ Bad - flashing content
const { events } = useTimeline({ roomId });
return <div>{events.length} messages</div>; // Shows 0 initially
Type Safety
All hooks are fully typed. Use TypeScript for best experience:
import type { XHubChatEvent, Room } from '@xhub-chat/core';
const { events }: { events: XHubChatEvent[] } = useTimeline({ roomId });
const { rooms }: { rooms: Room[] } = useRooms();
Reels Hooks
useMyReels
Fetch and manage the current user's own reels with filtering and pagination.
Type Signature:
function useMyReels(options?: UseMyReelsOptions): UseMyReelsReturn
interface UseMyReelsOptions {
autoLoad?: boolean;
limit?: number;
initialFilters?: MyReelsFilter;
}
interface MyReelsFilter {
approving_status?: 'approved' | 'rejected' | 'pending';
privacy?: number;
sort?: string;
sorted?: 'asc' | 'desc';
cursor?: string;
limit?: number;
}
interface UseMyReelsReturn {
reels: ReelData[];
loading: boolean;
error: Error | null;
hasMore: boolean;
filters: MyReelsFilter;
loadMore: () => Promise<void>;
reload: (newFilters?: MyReelsFilter) => Promise<void>;
setFilters: (newFilters: MyReelsFilter) => void;
}
Usage:
import { useMyReels } from '@xhub-chat/react';
function MyReelsPage() {
const { reels, loading, hasMore, loadMore } = useMyReels();
return (
<div>
{reels.map(reel => (
<ReelCard key={reel.id} reel={reel} />
))}
{hasMore && <button onClick={loadMore}>Load More</button>}
</div>
);
}
Parameters:
options.autoLoad- Auto-load reels on mount (default:true)options.limit- Number of items per page (default:10)options.initialFilters- Initial filter values
Returns:
reels- Array of loaded reelsloading- Loading stateerror- Error if anyhasMore- Whether more reels are availablefilters- Current filter valuesloadMore- Load next page of reelsreload- Reload from start with optional new filterssetFilters- Update filters and trigger reload
Example - With Filters:
function MyApprovedReels() {
const { reels, setFilters } = useMyReels({
initialFilters: { approving_status: 'approved' },
});
const showPending = () => {
setFilters({ approving_status: 'pending' });
};
return (
<div>
<button onClick={showPending}>Show Pending</button>
<ReelsList reels={reels} />
</div>
);
}
Example - Manual Load:
function MyReelsManual() {
const { reels, reload } = useMyReels({ autoLoad: false });
return (
<div>
<button onClick={() => reload()}>Load My Reels</button>
{reels.length > 0 && <ReelsList reels={reels} />}
</div>
);
}
See Also:
- My Reels Guide - Complete guide
- fetchMyReels - Core API method
Next Steps
- 📚 Using with React - Complete React guide
- 💡 API Reference - Core client API
- 🎨 Examples - Working examples