Integrated Chat in React
This quickstart is for React developers. It builds the same flow as part 2 with a provider and hooks: a cached token function, a conversation list, a message thread and live updates.
What you will build
Section titled “What you will build”A React app where the signed-in user picks a conversation, reads its messages newest first, sends a message and sees new messages arrive live. It uses the /react entry points of ccs-chat-user-client and ccs-chat-notifications-client. They need React 18 or later, as an optional peer dependency.
Prerequisites
Section titled “Prerequisites”-
The backend token route from part 2, step 2. Your app calls it to get a Chat token.
-
Both libraries installed from GitHub Packages (see part 1 for the
.npmrc):Terminal window npm install @central-code-station/ccs-chat-user-client @central-code-station/ccs-chat-notifications-client
conversations.readmessages.readmessages.sendUser scope: chat:access-
Add the public configuration.
environment.ts // Public values only. Nothing here is a secret, and the web app needs no client id for Chat.export const environment = {chatApiUrl: 'https://chat.centralcodestation.com/api/v1',tenantId: 'fitness',applicationId: 'fitness-app',chatTokenUrl: '/api/chat-token', // the route of your backend that exchanges the token}; -
Write the cached token function.
It calls your token route and keeps the token until a minute before it expires. Both libraries call
getAccessTokenbefore every request or reconnect.chat-token.ts import { environment } from './quickstart-environment';// The user's token for your own API, from your existing sign-in.declare function getFitnessAccessToken(): Promise<string>;let cached: { token: string; expiresAt: number } | null = null;// Pass this as getAccessToken. It renews a minute before expiry; there is no refresh token.export async function getChatAccessToken(): Promise<string> {if (cached && cached.expiresAt - 60_000 > Date.now()) return cached.token;const response = await fetch(environment.chatTokenUrl, {headers: { Authorization: `Bearer ${await getFitnessAccessToken()}` },});if (!response.ok) throw new Error('chat token unavailable');const { access_token, expires_in } = (await response.json()) as { access_token: string; expires_in: number };cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 };return access_token;} -
Provide the client and connect once.
Create the
ChatUserClientonce, outside your components.ChatUserClientProvidermakes it available to the hooks. CalluseChatNotificationsonce near the root: it creates the client, connects whenenabledis true and disconnects on unmount.useConversationsloads the list.ChatApp.tsx import { useState } from 'react';import { ChatUserClient } from '@central-code-station/ccs-chat-user-client';import { ChatUserClientProvider, useConversations } from '@central-code-station/ccs-chat-user-client/react';import { useChatNotifications } from '@central-code-station/ccs-chat-notifications-client/react';import { getChatAccessToken } from './quickstart-chat-token';import { environment } from './quickstart-environment';import { Thread } from './quickstart-react-thread';const context = {chatApiUrl: environment.chatApiUrl,tenantId: environment.tenantId,applicationId: environment.applicationId,getAccessToken: getChatAccessToken,};const chat = new ChatUserClient(context); // once, outside componentsexport function ChatApp({ signedIn }: { signedIn: boolean }) {// Connect once, near the root. It disconnects when signedIn turns false or the app unmounts.const { client: live, connectionState } = useChatNotifications({ ...context, enabled: signedIn });const [selected, setSelected] = useState<string | null>(null);return (<ChatUserClientProvider client={chat}><p>Live updates: {connectionState}</p><ConversationList onSelect={setSelected} />{selected && <Thread conversationId={selected} live={live} />}</ChatUserClientProvider>);}function ConversationList({ onSelect }: { onSelect: (conversationId: string) => void }) {const { items, loading } = useConversations({ limit: 30 });if (loading) return <p>Loading</p>;return (<ul>{items.map((c) => (<li key={c.conversation_id}><button onClick={() => onSelect(c.conversation_id)}>{c.name ?? c.conversation_id}</button></li>))}</ul>);} -
Show a thread and apply live messages.
useMessagesreturns the messages newest first.loadMorefetches older ones. Amessage.createdevent has the same message shape, so you prepend it withmutateand send no request.useChatNotificationEventuses the latest handler you pass on each render.Thread.tsx import type { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';import { useChatNotificationEvent } from '@central-code-station/ccs-chat-notifications-client/react';import { useChatUserClient, useMessages } from '@central-code-station/ccs-chat-user-client/react';export function Thread({ conversationId, live }: { conversationId: string; live: ChatNotificationsClient }) {const chat = useChatUserClient();const { items, loading, hasMore, loadMore, mutate } = useMessages(conversationId); // newest first// A live message goes to the top of the list without a request.useChatNotificationEvent(live, 'message.created', ({ message }) => {if (message.conversation_id === conversationId) mutate((messages) => [message, ...messages]);});const send = (body: string) => chat.messages.send(conversationId, { body });return (<div>{loading && <p>Loading</p>}{items.map((m) => (<p key={m.message_id}>{m.sender?.display_name ?? m.sender_user_id}: {m.body}</p>))}{hasMore && <button onClick={() => void loadMore()}>Load older</button>}<button onClick={() => void send('Hi!')}>Say hi</button></div>);}
What the hooks do and do not do
Section titled “What the hooks do and do not do”| Hook | Returns |
|---|---|
useChatUserClient() |
The client from the provider, for writes such as send and markRead. |
useCapabilities() |
The user’s Chat permissions, for showing or hiding controls. |
useConversations({ type?, limit? }) |
items, hasMore, loadMore, loading, error, reload, mutate. |
useMessages(conversationId, { limit? }) |
The same for messages. Nothing loads while conversationId is null. |
useChatRequest and useChatPages |
The generic forms, for any other call. |
The hooks reload when their inputs change and drop responses that arrive after a newer request. They are not a cache. If your app uses TanStack Query or SWR, call the client’s methods from those. Web push has its own hook, usePushRegistration; see Web push.
Verify
Section titled “Verify”- Sign in as a user that your backend added to a conversation. The list shows it.
- The text “Live updates: connected” shows once the stream opens.
- Select the conversation and select Say hi. The other member, signed in elsewhere, sees the message arrive without a reload.
If the connection state stays error or disconnected, check that the token route returns a token and that your origin is registered. See Troubleshooting.
Next steps
Section titled “Next steps”- Full React wiring with the provider, hooks and live inserts.
- Live updates and list and read.
- React Native is not covered: see Mobile and React Native.
- Package pages:
ccs-chat-user-clientfor React andccs-chat-notifications-clientfor React.