Skip to content

React in depth

The Chat libraries ship React adapters: a provider and data hooks for the user client, and hooks for live updates. This guide wires them together. For a first working result, use the React quickstart.

Render an inbox and a thread from hooks, and insert live messages into the thread without reloading it.

Create one ChatUserClient outside your components, or in useMemo, and pass it to ChatUserClientProvider. useConversations returns { items, hasMore, loadMore, loading, loadingMore, error, reload, mutate }. useCapabilities returns the user’s Chat permissions for showing or hiding controls.

app.tsx
import { ChatUserClient } from '@central-code-station/ccs-chat-user-client';
import { ChatUserClientProvider, useCapabilities, useConversations } from '@central-code-station/ccs-chat-user-client/react';
import { context } from './fe-guide-context';
// Create the client once, outside components.
const chat = new ChatUserClient(context);
export function App() {
return (
<ChatUserClientProvider client={chat}>
<Inbox onOpen={(conversationId) => console.log('open', conversationId)} />
</ChatUserClientProvider>
);
}
function Inbox({ onOpen }: { onOpen: (conversationId: string) => void }) {
const { items, loading, hasMore, loadMore, error } = useConversations({ limit: 30 });
const { data: me } = useCapabilities();
const canStartChats = me?.capabilities.includes('conversations.create') ?? false;
if (error) return <p>Could not load your conversations.</p>;
return (
<ul>
{loading && <li>Loading...</li>}
{canStartChats && <li><button>New chat</button></li>}
{items.map((c) => (
<li key={c.conversation_id} onClick={() => onOpen(c.conversation_id)}>
{c.name ?? c.type} {c.has_unread ? '(new)' : ''}
</li>
))}
{hasMore && <li><button onClick={() => void loadMore()}>Load more</button></li>}
</ul>
);
}

Call useChatNotifications near the root. It creates the client, connects while enabled is true and disconnects on unmount. It returns { client, connectionState, subscribeToConversation, unsubscribeFromConversation }. Put client in your own context so threads can listen to it.

live-updates-provider.tsx
import { createContext, useContext, type ReactNode } from 'react';
import { useChatNotifications } from '@central-code-station/ccs-chat-notifications-client/react';
import type { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';
import { context } from './fe-guide-context';
const LiveContext = createContext<ChatNotificationsClient | null>(null);
/** Connect once, near the root, and pass the client down. */
export function LiveUpdatesProvider({ signedIn, children }: { signedIn: boolean; children: ReactNode }) {
const { client, connectionState } = useChatNotifications({ ...context, enabled: signedIn });
return (
<LiveContext.Provider value={client}>
{connectionState === 'error' && <p>Live updates are paused.</p>}
{children}
</LiveContext.Provider>
);
}
export function useLiveClient(): ChatNotificationsClient {
const client = useContext(LiveContext);
if (!client) throw new Error('Wrap your app in <LiveUpdatesProvider>');
return client;
}

The hook creates a new client when chatApiUrl, tenantId or applicationId changes. It reads getAccessToken fresh on every reconnect, so the function does not need a stable identity.

useMessages(conversationId) loads newest first and pages with loadMore(). Nothing loads while conversationId is null. mutate changes the loaded items locally. Prepend each message.created of the open thread. Skip ids you already hold, because a sender can receive their own message back. Mark message.deleted ones as tombstones. useChatNotificationEvent uses the latest handler on every render.

thread.tsx
import { useChatUserClient, useMessages } from '@central-code-station/ccs-chat-user-client/react';
import { useChatNotificationEvent } from '@central-code-station/ccs-chat-notifications-client/react';
import { useLiveClient } from './fe-guide-react-live';
export function Thread({ conversationId }: { conversationId: string }) {
const chat = useChatUserClient();
const live = useLiveClient();
const { items, hasMore, loadMore, mutate } = useMessages(conversationId); // newest first
useChatNotificationEvent(live, 'message.created', ({ conversation_id, message }) => {
if (conversation_id !== conversationId) return;
// The sender can get their own message back too, so skip ids you already have.
mutate((messages) => (messages.some((m) => m.message_id === message.message_id) ? messages : [message, ...messages]));
});
useChatNotificationEvent(live, 'message.deleted', ({ conversation_id, message_id }) => {
if (conversation_id !== conversationId) return;
mutate((messages) => messages.map((m) => (m.message_id === message_id ? { ...m, status: 'DELETED', body: '' } : m)));
});
return (
<div>
{hasMore && <button onClick={() => void loadMore()}>Load older</button>}
{[...items].reverse().map((m) => (
<p key={m.message_id}>{m.status === 'DELETED' ? 'Message deleted' : m.body}</p>
))}
<button onClick={() => void chat.messages.send(conversationId, { body: 'See you at 9!' })}>Send</button>
</div>
);
}

Events during a reconnect are not replayed. Call the hook’s reload() when connectionState becomes connected again. See Live updates.

The hooks reload when their inputs change and drop responses that arrive after a newer request. They do not share results between components or keep them after unmount. If your app already uses TanStack Query or SWR, call the client’s methods from their fetch functions and use the hooks only for live inserts. The generic useChatRequest(load, deps) and useChatPages(loadPage, deps) wrap any other call.

Open a thread. Send a message from another user. It appears at the end of the thread without a reload. Delete it there and it turns into “Message deleted”.

You see Cause Fix
useChatUserClient: wrap your components in <ChatUserClientProvider> A hook ran outside the provider. Move the provider above it.
The thread refetches on every render deps of a generic hook change identity. Pass primitive values in deps.
Duplicate messages Your own send and the live event both inserted it. Skip a message whose message_id is already in the list.
No live events in a thread The client was not connected, or the thread listens to another client. Share the client from one useChatNotifications.