React SDK
@cstar.help/react wraps the JS client in idiomatic React hooks. Loading, error, and refetch
states are handled for you. Two provider trees (one for authenticated chat, one for the public knowledge
base) so customer-facing surfaces and admin surfaces stay separate.
Installation
npm install @cstar.help/react Pulls in @cstar.help/js as a transitive dep. Needs React 18 or newer. Real-time messaging
is on by default. No extra package to install.
Browse by topic
Drilling into something specific? Topics with a dedicated sub-page are linked below; the rest scroll to the matching section on this page.
Setup
Chat hooks
Library hooks
Recipes
Two providers, two surfaces
Chat needs authentication and runs over real-time. Library is public, no auth, fine to render statically. They live in separate provider trees so you can mount one, the other, or both, without one bleeding state into the other.
CStarChatProvider
Wrap your app (or chat widget) with the chat provider to enable customer tickets and messages.
Only teamSlug is required. Real-time delivery is enabled by default with an automatic
polling fallback.
import { CStarChatProvider } from '@cstar.help/react';
export default function Providers({ children }) {
return (
<CStarChatProvider teamSlug="acme">
{children}
</CStarChatProvider>
);
} Pass realtime=false to disable the live connection and poll instead (every 3 seconds
by default). Real-time is recommended for production.
Chat Hooks
useChat
Identify customers and manage the chat session. Must call identify() with an HMAC signature
before using other chat hooks.
import { useChat } from '@cstar.help/react';
function ChatWidget() {
const {
identify, // (customer, hmacSignature) => Promise<IdentifyResult>
disconnect, // () => void
isIdentified, // boolean
isRealtimeReady, // boolean
error // Error | null
} = useChat();
async function startChat() {
await identify({
externalId: 'user_123',
email: 'jane@example.com',
name: 'Jane Doe',
timestamp: Math.floor(Date.now() / 1000)
}, 'hmac_signature_from_your_server');
}
if (!isIdentified) {
return <button onClick={startChat}>Start Chat</button>;
}
return <div>Chat ready! {isRealtimeReady ? '(live)' : '(polling)'}</div>;
} useTickets
List and create the customer's tickets.
import { useTickets } from '@cstar.help/react';
function TicketList() {
const {
tickets, // Ticket[]
isLoading, // boolean
error, // Error | null
hasMore, // boolean
refresh, // () => Promise<void>
create // (params) => Promise<Ticket>
} = useTickets();
async function startNew() {
const ticket = await create({
title: 'Help with billing',
message: 'I have a question about my invoice.'
});
console.log('Created:', ticket.id);
}
if (isLoading) return <p>Loading...</p>;
return (
<div>
{tickets.map(t => (
<div key={t.id}>
<h3>{t.title}</h3>
<span>{t.status} · {t.messageCount} messages</span>
</div>
))}
<button onClick={startNew}>New Ticket</button>
</div>
);
} useMessages
Send and receive messages on a ticket. Includes optimistic updates and real-time delivery.
import { useMessages } from '@cstar.help/react';
function MessageThread({ ticketId }) {
const {
messages, // Message[]
isLoading, // boolean
error, // Error | null
send, // (content: string) => Promise<Message>
refresh // () => Promise<void>
} = useMessages(ticketId);
const [draft, setDraft] = useState('');
async function handleSend() {
await send(draft); // Optimistic: appears instantly
setDraft('');
}
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.sender_name}</strong>
<p>{msg.content}</p>
</div>
))}
<input value={draft} onChange={e => setDraft(e.target.value)} />
<button onClick={handleSend}>Send</button>
</div>
);
} useTyping
Show typing indicators for agents. Indicators auto-clear after 4 seconds of inactivity.
import { useTyping } from '@cstar.help/react';
function TypingIndicator({ ticketId }) {
const {
typingAgents, // { agentId, agentName }[]
sendTyping // (isTyping: boolean) => Promise<void>
} = useTyping(ticketId);
if (typingAgents.length === 0) return null;
const names = typingAgents.map(a => a.agentName).join(', ');
return <p>{names} is typing...</p>;
} CStarLibraryProvider
Wrap your help center with the library provider. No authentication needed. The knowledge base is public.
import { CStarLibraryProvider } from '@cstar.help/react';
export default function HelpLayout({ children }) {
return (
<CStarLibraryProvider teamSlug="acme">
{children}
</CStarLibraryProvider>
);
} Library Hooks
useCategories
import { useCategories } from '@cstar.help/react';
function CategoryList() {
const { categories, isLoading, error, refresh } = useCategories();
if (isLoading) return <p>Loading...</p>;
return (
<div>
{categories.map(cat => (
<a key={cat.id} href={`/help/${cat.slug}`}>
{cat.name} ({cat.article_count} articles)
</a>
))}
</div>
);
} useArticles
import { useArticles } from '@cstar.help/react';
function ArticleList({ categorySlug }) {
const { articles, isLoading } = useArticles({
categorySlug,
limit: 10
});
if (isLoading) return <p>Loading...</p>;
return (
<div>
{articles.map(article => (
<a key={article.id} href={`/help/articles/${article.slug}`}>
<h3>{article.title}</h3>
<p>{article.excerpt}</p>
</a>
))}
</div>
);
} useArticle
import { useArticle } from '@cstar.help/react';
function ArticlePage({ slug }) {
const { article, isLoading, error } = useArticle(slug);
if (isLoading) return <p>Loading...</p>;
if (!article) return <p>Article not found</p>;
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
} useArticleSearch
Full-text search with built-in 300ms debounce.
import { useArticleSearch } from '@cstar.help/react';
function SearchBar() {
const [query, setQuery] = useState('');
const { results, totalCount, isLoading } = useArticleSearch(query);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search articles..."
/>
{isLoading && <p>Searching...</p>}
<p>{totalCount} results</p>
{results.map(article => (
<a key={article.id} href={`/help/articles/${article.slug}`}>
{article.title}
</a>
))}
</div>
);
} Community Hooks
Wrap in <CStarCommunityProvider> for public community forum access. No auth required.
CStarCommunityProvider
import { CStarCommunityProvider } from '@cstar.help/react';
function App() {
return (
<CStarCommunityProvider teamSlug="acme">
<CommunityForum />
</CStarCommunityProvider>
);
} useTopics
import { useTopics } from '@cstar.help/react';
function TopicNav() {
const { topics, isLoading } = useTopics();
return (
<nav>
{topics.map(topic => (
<a key={topic.id}>{topic.name} ({topic.slug})</a>
))}
</nav>
);
} usePosts
import { usePosts } from '@cstar.help/react';
function PostList() {
const { posts, count, isLoading } = usePosts({
topicSlug: 'feature-requests',
sort: 'votes'
});
return (
<div>
<p>{count} posts</p>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<span>{post.voteCount} votes · {post.commentCount} comments</span>
</div>
))}
</div>
);
} usePost
import { usePost } from '@cstar.help/react';
function PostPage({ slug }) {
const { data, isLoading, error } = usePost(slug);
if (!data) return null;
return (
<>
<h1>{data.post.title}</h1>
<p>{data.post.body}</p>
{data.comments.map(c => (
<div key={c.id}>
<strong>{c.authorName}</strong>: {c.body}
</div>
))}
</>
);
} useCommunitySearch
import { useCommunitySearch } from '@cstar.help/react';
function CommunitySearch() {
const { results, isLoading, search } = useCommunitySearch();
return (
<>
<input onChange={e => search(e.target.value)} placeholder="Search posts..." />
{results?.data.map(post => (
<a key={post.id}>{post.title}</a>
))}
</>
);
}