← Developer guides

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.