Skip to main content

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/react

This 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.

app/providers.tsx
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.

app/help/layout.tsx
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>
      ))}
    </>
  );
}