@growblic/client-react
React hooks for the Growblic chat SDK: live conversations, messages, typing, and connection state.
npm install @growblic/client-react @growblic/client-core
Peer dependencies: react and react-dom ≥ 18. Requires Node ≥ 20 to build/SSR.
Setup
Create one client (see @growblic/client-core for the token model) and provide it:
import { createClient } from "@growblic/client-core";
import { GrowblicProvider } from "@growblic/client-react";
const client = createClient({
apiUrl: "https://api.your-growblic-host.com",
wsUrl: "wss://api.your-growblic-host.com/socket",
tokenProvider: async () => (await fetch("/api/growblic-token", { method: "POST" })).json(),
});
export function App() {
return (
<GrowblicProvider client={client}>
<Chat />
</GrowblicProvider>
);
}
Hooks
| Hook | Gives you |
|---|---|
useConversations() |
The live inbox: { conversations, loading, error, refresh } — prepends new conversations, updates unread/title/preview rows live |
useMessages(conversationId) |
The watched channel's readonly Message[], live-updating (new/edited/deleted) |
useChannel(conversationId) |
{ channel, ready } — the channel handle (send, react, markRead, loadMessages) + backfill readiness |
useTyping(conversationId) |
The user ids currently typing in this conversation |
useConnectionState() |
"offline" | "connecting" | "connected" | "reconnecting" for a status pill |
usePresence(userIds) |
Live { [userId]: { online, lastSeen? } } for a set of users — snapshot + subscription, privacy-filtered server-side |
useViewing(conversationId) |
The user ids currently VIEWING this conversation (like typing, but "has it open") |
useCall() |
The single-active-call state machine: status, call, token, and start / accept / reject / end / reset |
Minimal chat
import { useChannel, useConversations, useMessages } from "@growblic/client-react";
function Chat() {
const { conversations } = useConversations();
const id = conversations[0]?.id;
if (!id) return <p>No conversations yet.</p>;
return <Thread conversationId={id} />;
}
function Thread({ conversationId }: { conversationId: string }) {
const messages = useMessages(conversationId);
const { channel } = useChannel(conversationId);
return (
<>
<ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>
<button onClick={() => channel.sendMessage({ text: "hi" })}>Send</button>
</>
);
}
Presence
import { usePresence, useViewing } from "@growblic/client-react";
function Roster({ memberIds }: { memberIds: string[] }) {
// Pass a fresh array every render — the hook keys on the SET, not array identity (no resubscribe storm).
const presence = usePresence(memberIds);
return (
<ul>
{memberIds.map((id) => (
<li key={id}>
{id} {presence[id]?.online ? "🟢" : presence[id]?.lastSeen ?? "offline"}
</li>
))}
</ul>
);
}
function Header({ conversationId }: { conversationId: string }) {
const viewing = useViewing(conversationId);
return viewing.length > 0 ? <p>{viewing.length} viewing now</p> : null;
}
A user you're not permitted to see simply reads offline (privacy is enforced server-side); the snapshot endpoint caps at 200 ids per request.
Calls
useCall() runs the whole lifecycle; connecting media is your job (pair token — {url, token} — with
call.room in your LiveKit client; this package has no livekit dependency):
import { useCall } from "@growblic/client-react";
function CallUI() {
const { status, call, token, endedReason, start, accept, reject, end, reset } = useCall();
if (status === "idle") return <button onClick={() => start("bob", "voice")}>Call Bob</button>;
if (status === "ringing")
return (
<>
<p>{call?.peerName ?? "Someone"} is calling…</p>
<button onClick={accept}>Answer</button>
<button onClick={reject}>Decline</button>
</>
);
if (status === "connected") return <button onClick={end}>Hang up</button>; // connect LiveKit with {token, call.room}
if (status === "ended") return <button onClick={reset}>Done ({endedReason})</button>;
return <p>{status}…</p>; // dialing / connecting
}
One active call at a time: a call.incoming while you're already dialing/connected is ignored by the hook
(listen to the raw client.on("call.incoming") if you want call-waiting UX). Note: call.peerId on an
incoming call is currently the backend's internal id, not an external id — use peerName for display.
MIT Growblic