Skip to main content

Hooks

Six hooks. Every one returns the standard HookState<T> shape (except useEvent, which is imperative-only).

interface HookState<T> {
data: T | undefined;
error: QuestKitError | null;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
refetch: () => Promise<void>;
}

isLoading / isSuccess / isError are mutually exclusive. data is T | undefined while loading and after a refetch; null is reserved for "the server has no row" (e.g. an unminted balance).


useMissions(opts?)

Fetches a list of missions and the user's progress for each. Keeps progress live by folding in mission.progress / mission.completed SSE updates.

import { useMissions } from "@questkit/react";

function MissionListView() {
const { data, isLoading, isError, refetch } = useMissions({ status: "active" });

if (isLoading) return <p>Loading…</p>;
if (isError) return <button onClick={refetch}>Retry</button>;

return (
<ul>
{data?.missions.map((m) => (
<li key={m.id}>
{m.title}{Math.round((data.progress[m.id]?.progress ?? 0) * 100)}%
</li>
))}
</ul>
);
}

Options

FieldTypeDescription
campaignIdstringFilter to a single campaign.
status"active" | "completed" | "claimed" | "locked" | "all"Filter by progress status.
limitnumberServer-side page size.
cursorstringOpaque pagination cursor returned by the previous page.

Returns HookState<{ missions: Mission[]; progress: Record<string, MissionProgress>; nextCursor?: string }>.


useMission(id)

Fetches a single mission + its progress. The progress field updates on incoming SSE messages filtered by missionId === id.

const { data } = useMission("daily-streak");
// data?.mission, data?.progress

Returns HookState<{ mission: Mission; progress: MissionProgress | null }>.


useBalance(currency?)

Fetches the user's balance for one currency, or all currencies when called with no argument. Stays live via balance.changed SSE updates.

const { data } = useBalance("gold"); // HookState<Balance | null>
const { data: all } = useBalance(); // HookState<Balance[]>
Call signaturedata typeSSE behaviour
useBalance(code)Balance | nullUpdates only when the SSE message matches that currency.
useBalance()Balance[]Upserts every balance.changed update by currency code.

null in single-currency mode means "no row on the server yet"; render as 0.


useEvent()

The only imperative hook. Returns an action you call from event handlers — no data field, no SSE subscription.

import { useEvent } from "@questkit/react";

function BuyButton() {
const { fireEvent, isFiring, error } = useEvent();

return (
<button
disabled={isFiring}
onClick={() =>
fireEvent({ name: "purchase.completed", payload: { sku: "boots-01" } })
}
>
{isFiring ? "Sending…" : "Buy"}
</button>
);
}

Return shape

FieldTypeDescription
fireEvent(input: FireEventInput) => Promise<FireEventResult>Fires one event. Rejects on failure.
isFiringbooleanTrue while a fireEvent call is in flight.
errorQuestKitError | nullThe most recent failure (cleared on next call).

useCampaign(id?)

Fetches a single campaign (with missions) or the list of campaigns. No SSE coupling — campaigns are catalog data. Call refetch() if a curator updates a campaign mid-session.

const { data } = useCampaign("spring-2026"); // HookState<{ campaign, missions? }>
const { data: list } = useCampaign(); // HookState<Campaign[]>

useRecommendations()

Calls /v1/recommendations for AI-curated mission suggestions. Two cache layers protect the AI binding:

  • Server cache (KV, 1 hour) — the cached flag in the response indicates a server hit.
  • Client cache (in-memory, 5 minutes) — shared across all mounts of <RecommendedMissions /> in the same app.
const { data, isLoading, refetch } = useRecommendations();
// data?.missionIds, data?.reason, data?.cached

refetch() bypasses the client cache (handy after a major user action). The client cache is auto-invalidated when the SDK receives an SSE recommendation update.

Returns HookState<{ missionIds: string[]; reason: string; cached: boolean; count: number }>.