Query Provider
About
React Query is often configured with assumptions that work well for REST dashboards: background refetching, automatic retries, and cache freshness windows measured in seconds or minutes.
Blockchain applications tend to need a different model. State changes are often driven by explicit transactions, and data refresh is usually safest when tied to known lifecycle points such as “a mutation just succeeded” or “a block-based event finished.”
The QueryProvider pattern gives the app one React Query client configured around those assumptions.
Why the pattern matters
If background cache behavior is too aggressive in a Vara app, the UI can become noisy or misleading:
- a tab focus event may trigger an unexpected RPC read,
- retries may hide deterministic contract errors,
- stale/fresh heuristics may conflict with explicit transaction-driven invalidation.
This pattern chooses predictability over automation.
Core code
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 0,
staleTime: Infinity,
refetchOnWindowFocus: false,
retry: false,
},
},
});
export function QueryProvider({ children }: PropsWithChildren) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}The implementation is small, but the defaults are opinionated.
Default configuration explained
Infinite Stale Time
Queries are treated as fresh until the app explicitly invalidates them.
That matches blockchain UX well because successful transactions are often the natural invalidation trigger.
Immediate Garbage Collection
Once no component is observing a query, React Query may garbage-collect it immediately.
This keeps the cache lifecycle easy to reason about: active screens hold data; inactive screens re-read or get invalidated explicitly.
No Refetch on Window Focus
Switching tabs should not silently trigger a new round of chain queries unless the app deliberately wants that behavior.
No Automatic Retry
In blockchain flows, many errors are deterministic:
- bad arguments,
- contract rejection,
- insufficient balance,
- invalid reply decoding assumptions.
Automatically retrying those failures often makes the UX worse rather than better.
How this pattern fits into the larger flow
In a typical Vara app:
- Query data is loaded and held by React Query.
- A mutation executes a transaction.
- After success, the app invalidates only the affected queries.
That gives the app an explicit cause-and-effect model instead of a background polling model.
Mutation-driven invalidation example
The intended usage is to invalidate data after known successful execution:
const queryClient = useQueryClient();
const execute = useProgramTxMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['program-state', programId] });
},
});That is different from “refetch whenever the tab gains focus.” The UI updates because a specific chain operation succeeded.
When to override defaults
Some screens may still need more aggressive behavior. For example, a block explorer-style screen may want polling:
useQuery({
queryKey: ['latest-events'],
queryFn: fetchLatestEvents,
refetchInterval: 3000,
});The pattern keeps global defaults conservative and lets individual queries opt into special behavior.
Production guidance
- Invalidate queries after known successful mutations instead of relying on freshness timers.
- Add query-specific overrides only when a screen truly needs different behavior.
- Keep the global client conservative. It is easier to opt a query into more aggressive behavior than to unwind too much global automation later.