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.
Prerequisites
Section titled “Prerequisites”- React 18 or later. See SDKs overview.
- A cached
getAccessToken. See Get the user’s Chat token.
1. Provide the client
Section titled “1. Provide the client”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.
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> );}2. Connect live updates once
Section titled “2. Connect live updates once”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.
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.
3. Insert live messages with mutate
Section titled “3. Insert live messages with mutate”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.
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> );}4. Reload after a reconnect
Section titled “4. Reload after a reconnect”Events during a reconnect are not replayed. Call the hook’s reload() when connectionState becomes connected again. See Live updates.
Hooks are not a cache
Section titled “Hooks are not a cache”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.
Verify
Section titled “Verify”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”.
Troubleshooting
Section titled “Troubleshooting”| 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. |