React SPA integration
Wire the Quravin SDK into a React component with a small custom hook — one client instance, real loading/error state, no re-instantiation on every render.
The SDK has no React-specific build — it’s the same CDN client any plain webpage uses. What changes in a React app is where the client instance lives and how you expose loading/error state to components. This guide covers both.
1. Load the SDK
import { Quravin } from "@quravin/sdk" ergonomics aren’t on the public npm registry yet — ask
your platform operator if you need that path. Today, load it the same way Plain webpage
integration does: the classic <script src> tag works inside a
bundled SPA too — add it to index.html, then read the client off window.Quravin.Quravin (the
double name is awkward; a future SDK version smooths it out).
<!-- index.html -->
<script src="https://js.quravin.com/v1.js"></script>
2. Get a session token from your own backend
Same rule as any browser code: never put a static x-api-key in a React app — anyone can read
it out of the bundle. Your backend mints a short-lived, per-user session JWT and hands it to the
page. If you don’t already have a token-minting endpoint, Node.js server
integration shows how to build one (the pattern is
the same regardless of your backend’s language — only the endpoint’s implementation differs).
async function fetchSessionToken() {
const r = await fetch("/api/ai-token", { credentials: "include" });
if (!r.ok) throw new Error("Token fetch failed");
return (await r.json()).token;
}
3. Create the client once — not on every render
A common mistake: instantiating new window.Quravin.Quravin(...) inside a component body creates a new
client on every render, which drops any in-flight onTokenExpired retry logic. Create it once,
using a ref or module scope:
import { useRef } from "react";
// window.Quravin is the CDN script's global (see Step 1) — there's no npm type
// import yet, so declare just enough shape for what this hook uses.
type QuravinClient = { run(args: { pipeline: string; inputs: Record<string, unknown> }): Promise<unknown> };
declare global {
interface Window {
Quravin: {
Quravin: new (opts: {
endpoint: string;
sessionToken: string;
onTokenExpired: () => Promise<string>;
}) => QuravinClient;
};
}
}
function useQuravinClient() {
const clientRef = useRef<QuravinClient | null>(null);
if (!clientRef.current) {
clientRef.current = new window.Quravin.Quravin({
endpoint: import.meta.env.VITE_QURAVIN_API_BASE, // your bundler's env-var convention
sessionToken: "", // placeholder — set for real on first fetchSessionToken() below
onTokenExpired: fetchSessionToken,
});
}
return clientRef.current;
}
In practice, most apps fetch the token once on mount (or on login) rather than lazily on first call — see the full hook below for a version that does that.
4. A small hook that owns run state
Components shouldn’t each re-implement loading/error/result state. One hook, reused everywhere:
import { useCallback, useRef, useState } from "react";
type RunState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "error"; error: string }
| { status: "done"; result: T };
export function useQuravin() {
const clientRef = useRef<QuravinClient | null>(null);
const getClient = useCallback(() => {
if (!clientRef.current) {
clientRef.current = new window.Quravin.Quravin({
endpoint: import.meta.env.VITE_QURAVIN_API_BASE,
sessionToken: "",
onTokenExpired: fetchSessionToken,
});
}
return clientRef.current;
}, []);
const [state, setState] = useState<RunState<unknown>>({ status: "idle" });
const run = useCallback(
async (pipeline: string, inputs: Record<string, unknown>) => {
setState({ status: "loading" });
try {
const result = await getClient().run({ pipeline, inputs });
setState({ status: "done", result });
return result;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setState({ status: "error", error: message });
throw err;
}
},
[getClient],
);
return { state, run };
}
getClient() still constructs the client lazily with an empty sessionToken — swap in a real
mint-on-mount flow (fetch the token once in a useEffect and store it) once you’ve wired your own
/api/ai-token route; the shape above is deliberately minimal so it’s obvious where to plug that
in.
5. Use it in a component
function TranslateBox() {
const { state, run } = useQuravin();
const [text, setText] = useState("");
const onTranslate = () =>
run("translate-string", { text, target_language: "de" }).catch(() => {
/* state.error already holds the message */
});
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={onTranslate} disabled={state.status === "loading"}>
Translate
</button>
{state.status === "error" && <p role="alert">{state.error}</p>}
{state.status === "done" && <p>{(state.result as { translation: string }).translation}</p>}
</div>
);
}
Batching multiple inputs
For running the same pipeline across many inputs (e.g. translating every row of a table), use
ai.runMany(...) instead of one ai.run() call per item — it handles bounded concurrency and
per-item progress for you. The SDK’s fuller Integration Guide (ask your platform operator for a
copy — it isn’t public) documents the full options shape (concurrency, errorStrategy,
onProgress) and the per-pipeline result-shape table.
Error handling
ai.run() rejects on FAILED/CANCELED tickets and on HTTP errors — the hook above already
routes both into state.error. See Tool reference’s Errors section
for the full list of status codes and what each one means for the authenticated path.