Headless SDK: state and cache
Every hook in the SDK shares the same loading, error, and polling shape. This page covers how the cache works, when to refetch, and how to do optimistic updates without rolling your own state library.
Key takeaways
Quick read- All hooks return isLoading, error, and a refetch function. Same shape, every time.
- The SDK ships a built-in cache keyed by tenant + participant + resource. No external library needed.
- Polling defaults: 15s for active surfaces (challenge), 30s for ambient (points, tier), 5s during contest final hour.
- submit and claim auto-invalidate related caches. You rarely call refetch directly.
- On window.focus, every active hook revalidates. Stale data after a tab switch is rare.
Anatomy
The shape every hook returns
// Every hook returns at minimum:
{
isLoading: boolean; // first load only
isFetching: boolean; // any background revalidation
error: Error | null; // last error, if any
refetch: () => Promise<void>;
}
// Plus resource-specific data:
{
available: number;
lifetime: number;
// ... etc
}Polling
Defaults and overrides
Set per-hook
Pass pollInterval={milliseconds}. Use 0 to disable polling for offscreen widgets.
Adapt by context
Inside a contest's final hour, useLeaderboard auto-tightens to 5s. You can mirror this manually with pollInterval={lastHour ? 5000 : 30000}.
Pause when hidden
The Provider listens to document.visibilityState. Hidden tabs stop polling automatically; visible tabs resume.
Manual refetch
Call refetch() after an explicit user action that the SDK could not have observed (e.g. a flow you control end-to-end without an event).
Cache keys
How invalidation works
Cache key shape:
tenant : participant : resource [: subId]
Examples:
tenant_xyz : p_8a3f : points
tenant_xyz : p_8a3f : tier
tenant_xyz : p_8a3f : challenge : onboarding_v3
tenant_xyz : p_8a3f : leaderboard : april_quiz_cup : bracket
When a mutation lands, it invalidates affected keys:
submit() in useChallenge -> invalidates the same challenge + points + tier
claim() in useRewards -> invalidates rewards + points
spendFreeze() in useStreak -> invalidates the streak + points
You rarely call refetch directly. The SDK keeps state coherent.Optimistic updates
Render before the server confirms
const { available, refetch } = usePoints();
async function onPurchaseDone() {
// Local UI: pretend points have arrived
optimisticAdd(50);
// Tell the server
await emitFromServerSide(/* ... */);
// SDK auto-revalidates within ~1s, but you can force it:
await refetch();
}Most flows do not need optimistic updates. Reserve them for moments where the user just acted and waiting feels broken.
SSR
Hydration without flicker
// Pass an initial state from the server to skip the loading flash
import { fetchInitialState } from "@bricqs/headless-react/server";
const initial = await fetchInitialState({
apiKey: process.env.BRICQS_ADMIN_KEY!,
tenantId: process.env.BRICQS_TENANT!,
participantId: userId,
});
return (
<BricqsProvider {...providerProps} initialState={initial}>
{children}
</BricqsProvider>
);initialState seeds the cache. The first render uses real data instead of skeletons; revalidation runs in the background.
Common mistakes
What goes wrong
Wrapping every hook in useEffect to refetch on focus.
The SDK already does this. Remove the manual code.
Calling refetch in render. Infinite loop.
refetch is for event handlers and effects with deps, never bare in the render path.
Treating isFetching as isLoading. UI flickers on every poll.
isLoading is true only on first load. Use it for skeletons. isFetching is for subtle indicators (spinner in the header).
Polling secondary screens at 5 second intervals. Network tab is overwhelmed.
Default 30 to 60 seconds for ambient widgets. Tighten only for active flows.
Developer FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
