SDKReact hooks
React hooks
`useChat()` and `useCompletion()` for client-side chat UIs — with the right key-handling pattern.
@cleverrouter/sdk/react exposes two hooks for client-side chat UIs,
shaped after the Vercel AI SDK equivalents. They talk to CleverRouter
directly via our typed stream helper.
API keys never go to the browser
These hooks accept a config.apiKey at runtime. That key must come
from a short-lived token issued by your backend — never from a
build-time env var that ships in the JS bundle. See
Authentication.
At a glance
| Install | bun add @cleverrouter/sdk@alpha react |
| Hooks | useChat() (history), useCompletion() (single-shot) |
| Streaming | Built-in, with stop() to abort |
| Recommended pattern | Backend /api/cr-token issues ephemeral key |
useChat()
'use client';
import { useChat } from '@cleverrouter/sdk/react';
export function Chat({ apiKey }: { apiKey: string }) {
const {
messages,
input,
isLoading,
error,
handleInputChange,
handleSubmit,
stop,
reload,
} = useChat({
config: { apiKey },
model: 'mistral/mistral-small-3.2',
initialMessages: [
{ role: 'system', content: 'You are a helpful EU-savvy assistant.' },
],
onFinish: (msg) => console.log('done', msg),
});
return (
<div className="space-y-3">
<ul className="space-y-2">
{messages.map((m, i) => (
<li key={i}>
<b>{m.role}:</b>{' '}
{typeof m.content === 'string' ? m.content : '[parts]'}
</li>
))}
</ul>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
disabled={isLoading}
className="flex-1 rounded border px-3 py-2"
placeholder="Ask anything…"
/>
<button type="submit" disabled={isLoading}>Send</button>
{isLoading && (
<button onClick={stop} type="button">Stop</button>
)}
</form>
{error ? <p className="text-red-500">{String(error)}</p> : null}
<button onClick={reload} type="button">Regenerate</button>
</div>
);
}Returned helpers
| Field | Purpose |
|---|---|
messages | Current chat history (system / user / assistant) |
input | Controlled input value |
setInput | Programmatic input control |
setMessages | Replace the history (e.g. on conversation switch) |
isLoading | true while a stream is in flight |
error | Last thrown error (typed CR* class) |
handleInputChange | Wire to <input onChange> |
handleSubmit | Wire to <form onSubmit> |
append(msg) | Push a message and start a new completion |
reload() | Drop the last assistant turn and regenerate |
stop() | Abort the in-flight stream |
useCompletion()
Single-shot, no history:
'use client';
import { useCompletion } from '@cleverrouter/sdk/react';
export function Completion({ apiKey }: { apiKey: string }) {
const {
completion,
input,
isLoading,
handleInputChange,
handleSubmit,
stop,
} = useCompletion({
config: { apiKey },
model: 'mistral/mistral-small-3.2',
onFinish: (text) => console.log('final', text),
});
return (
<form onSubmit={handleSubmit} className="space-y-2">
<input value={input} onChange={handleInputChange} disabled={isLoading} />
<pre className="whitespace-pre-wrap">{completion}</pre>
{isLoading && <button onClick={stop} type="button">Stop</button>}
</form>
);
}Server-side token endpoint pattern
Issue an ephemeral key from your own backend; the client never sees a
long-lived cr_live_* key.
import { NextResponse } from 'next/server';
import { requireSession } from '@/lib/session';
export async function POST() {
const session = await requireSession();
// For now, the existing CR key from a server-only env var.
// Plan to migrate to per-session ephemeral tokens before GA.
return NextResponse.json({
apiKey: process.env.CLEVERROUTER_API_KEY!,
userId: session.user.id,
});
}'use client';
import { useEffect, useState } from 'react';
import { Chat } from './Chat';
export default function ChatPage() {
const [apiKey, setApiKey] = useState<string | null>(null);
useEffect(() => {
fetch('/api/cr-token', { method: 'POST' })
.then((r) => r.json())
.then(({ apiKey }) => setApiKey(apiKey));
}, []);
return apiKey ? <Chat apiKey={apiKey} /> : <p>Loading…</p>;
}Common gotchas
'use client'is mandatory. Both hooks read state and run effects.- Don't recreate the hook config every render. Memoise
configor it triggers a stream restart on every typing keystroke. - Stop before unmounting. The hooks cancel automatically on
unmount, but if you switch threads inside one mount, call
setMessages([])afterstop()to avoid orphan deltas.
Related
- SDK overview
- Authentication — server-only keys.
- Streaming — under-the-hood wire format.