React SDK
@cstar.help/react provides React hooks for customer-facing chat and public knowledge
base features. Built on top of @cstar.help/js with automatic loading states and error handling.
Installation
npm install @cstar.help/reactThis automatically installs @cstar.help/js (the core client) as a dependency.
Requires React 18+. For real-time chat, also install @supabase/supabase-js (optional
— falls back to polling without it).
Two Provider Architecture
The React SDK has two independent provider trees — one for chat (authenticated, real-time) and one for the knowledge base (public, no auth needed). Use one or both depending on your needs.
CStarChatProvider
Wrap your app (or chat widget) with the chat provider to enable customer conversations. Requires teamSlug and optionally Supabase config for real-time messaging.
import { CStarChatProvider } from '@cstar.help/react';
export default function Providers({ children }) {
return (
<CStarChatProvider
teamSlug="acme"
supabaseUrl={process.env.NEXT_PUBLIC_SUPABASE_URL}
supabaseAnonKey={process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}
>
{children}
</CStarChatProvider>
);
}Without Supabase config, chat automatically falls back to polling (every 3 seconds). 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>;
}useConversations
List and create customer conversations (tickets from the customer's perspective).
import { useConversations } from '@cstar.help/react';
function ConversationList() {
const {
conversations, // Conversation[]
isLoading, // boolean
error, // Error | null
hasMore, // boolean
refresh, // () => Promise<void>
create // (params) => Promise<Conversation>
} = useConversations();
async function startNew() {
const convo = await create({
subject: 'Help with billing',
message: 'I have a question about my invoice.'
});
console.log('Created:', convo.id);
}
if (isLoading) return <p>Loading...</p>;
return (
<div>
{conversations.map(c => (
<div key={c.id}>
<h3>{c.subject}</h3>
<span>{c.status} · {c.messageCount} messages</span>
</div>
))}
<button onClick={startNew}>New Conversation</button>
</div>
);
}useMessages
Send and receive messages in a conversation. Includes optimistic updates and real-time delivery.
import { useMessages } from '@cstar.help/react';
function MessageThread({ ticketId }) {
const {
messages, // WidgetMessage[]
isLoading, // boolean
error, // Error | null
send, // (content: string) => Promise<WidgetMessage>
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>
))}
</>
);
}