Skip to content

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.

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.

  • 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
Chat permissions (all of): conversations.readmessages.readmessages.sendUser scope: chat:access
  1. 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
    };
  2. Write the cached token function.

    It calls your token route and keeps the token until a minute before it expires. Both libraries call getAccessToken before 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;
    }
  3. Provide the client and connect once.

    Create the ChatUserClient once, outside your components. ChatUserClientProvider makes it available to the hooks. Call useChatNotifications once near the root: it creates the client, connects when enabled is true and disconnects on unmount. useConversations loads 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 components
    export 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>
    );
    }
  4. Show a thread and apply live messages.

    useMessages returns the messages newest first. loadMore fetches older ones. A message.created event has the same message shape, so you prepend it with mutate and send no request. useChatNotificationEvent uses 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>
    );
    }
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.

  1. Sign in as a user that your backend added to a conversation. The list shows it.
  2. The text “Live updates: connected” shows once the stream opens.
  3. 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.