React
How to use Leuria from React: the visitor's AI, conversations and the connect flow, as React state.
import { bridge, browserAI, createLeuria } from "@leuria/client"
import { LeuriaProvider, useConnect, useConversation } from "@leuria/react"
// Create the client once, outside render.
const ai = createLeuria({ providers: [bridge({ app: "My shop" }), browserAI()] })
export function App() {
return (
<LeuriaProvider client={ai}>
<Assistant />
</LeuriaProvider>
)
}
@leuria/react is headless: bring your own UI, or use @leuria/react-connect for Leuria's. The hooks read the client's immutable snapshots through useSyncExternalStore, so they work with React 18 and 19, StrictMode included.
LeuriaProvider and useLeuria
<LeuriaProvider client={ai}> makes a client available to the hooks below it. useLeuria() returns it, and throws when there is no provider above.
useLeuriaState
Every provider's state, active (what answers now) and pending (what a click would enable): the snapshot from ai.getState() (see Providers). Pass a selector to re-render only when part of it changes. useProvider(id) returns one provider's state.
const active = useLeuriaState((s) => s.active?.label)
const bridge = useProvider("bridge")
useConnect
The connect flow of one provider, by default the visitor's own AI through Leuria (connection() from @leuria/client, see Connect UI). It shows the same attempt as every other component and Leuria's Connect UI.
function Connect() {
const { status, model, connect, alternatives, chooseInstead, manage } = useConnect()
if (status === "connected") return <button onClick={manage}>Your AI · {model} · Change</button>
if (status === "not-running")
return (
<p>
<a href="https://leuria.eu" target="_blank" rel="noopener">Get Leuria</a>, then <button onClick={connect}>try again</button>
{alternatives.map((p) => <button key={p.id} onClick={() => chooseInstead(p.id)}>or use {p.label}</button>)}
</p>
)
return <button onClick={connect} disabled={status === "connecting"}>Connect your AI</button>
}
status is checking, not-running, not-connected, connecting, declined or connected. Call connect() from a click. disconnect() forgets the site's grant.
useConversation
A conversation that lives as long as the component: its state (messages, status, error, provider, session, queued, pendingInputs) plus send, stop, reset, warm, submitToolResult and rejectToolCall (see Conversations).
function Assistant() {
const { messages, status, send, stop } = useConversation({ system, tools: [searchProducts] })
return (
<>
{messages.map((m) => <Message key={m.id} message={m} />)}
<Composer onSend={(text) => send(text)} onStop={stop} running={status === "running"} />
</>
)
}
- Options are read on the first render. To change the system prompt or tools, remount (give the component a new
key) or callreset. - The conversation's session closes on unmount and on
pagehide; the history stays, and a latersendopens a new session. conversationis the underlyingConversation, e.g. forconversation.on().
useConversationState(conversation) follows a conversation created elsewhere.
useChat
One-shot requests (summaries, structured output) with their progress as state. start(request) returns the run, and stops a running one; stop() cancels.
function Summary({ page }: { page: string }) {
const { text, progress, start } = useChat()
return (
<>
<button onClick={() => start({ prompt: `Summarize:\n${page}` })}>Summarize</button>
{progress && <p>{progress}</p>}
<p>{text}</p>
</>
)
}
status is idle, running, done or error; text streams in; progress is worth showing while it starts (e.g. "Starting Codex…"). Stopping is not an error: the status goes back to idle. For structured output, await start({ prompt, schema }).object().
useExposedTools
Offers page tools to the browser's own agents (WebMCP) while the component is mounted (see Tools). Pass a stable array, defined outside render or memoized.
const tools = [searchProducts, addToCart]
function Shop() {
useExposedTools(tools)
…
}