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