Skip to content

ccs-chat-user-client for React

The /react entry point of ccs-chat-user-client adds a context provider and data hooks on top of ChatUserClient. It needs React 18 or later.

Install the package as described on the package page. react is an optional peer dependency, so your app already provides it. Import from the sub-path:

import { ChatUserClientProvider, useConversations, useMessages } from '@central-code-station/ccs-chat-user-client/react';

Create one ChatUserClient outside your components, or in useMemo, and pass it to ChatUserClientProvider. Hooks read it from context.

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>
);
}
Hook Returns
useChatUserClient() The client from the provider, for writes such as send and markRead. It throws outside the provider.
useCapabilities() { data, loading, error, reload, mutate } with the user’s Chat permissions.
useConversations({ type?, limit? }) { items, hasMore, loadMore, loading, loadingMore, error, reload, mutate }.
useMessages(conversationId, { limit? }) The same, newest first. Nothing loads while conversationId is null or undefined.
useChatRequest(load, deps, options?) Any one request, with { data, loading, error, reload, mutate }. options.enabled pauses it.
useChatPages(loadPage, deps, options?) Any cursor-paged list, with the same result as useConversations.

The hooks reload when their inputs change and drop a response that arrives after a newer request. mutate updates the loaded data locally without a request, which is how you insert a live message.

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>
);
}

useChatRequest(load, deps, options?) loads one request and reloads it when deps change. load needs no stable identity: the latest one is used. Pass enabled: false to pause it.

client-search.tsx
import { useChatRequest } from '@central-code-station/ccs-chat-user-client/react';
/** Any call can use the generic hook. It reloads when `deps` change and pauses while `enabled` is false. */
export function ClientSearch({ query }: { query: string }) {
const { data, loading, error } = useChatRequest((chat) => chat.users.lookup(query), [query], { enabled: query.length >= 2 });
if (error) return <p>Search is not available.</p>;
return (
<ul>
{loading && <li>Searching...</li>}
{data?.map((user) => <li key={user.user_id}>{user.display_name}</li>)}
</ul>
);
}

useChatNotifications and useChatNotificationEvent come from ccs-chat-notifications-client for React.

  • The hooks are not a cache. They do not share results between components or keep them after unmount. If your app uses TanStack Query or SWR, call the client’s methods from those instead.
  • It has no mutation hooks. Use useChatUserClient() for writes and update the list with mutate.
  • It has no live updates. It loads data in effects, so nothing is fetched during a server render.
  • It does not get the token for you. See Get the user’s Chat token.