Reducing Unnecessary Network Requests with React Query staleTime
Reducing Unnecessary Network Requests with React Query staleTime
Release: v0.1.237 · Category: Performance · Frontend
File affected:src/lib/trpc/provider.tsx
Background
The platform uses tRPC together with TanStack React Query to fetch data from the server. Every tRPC hook (useQuery) is backed by a React Query cache entry.
React Query's default staleTime is 0 — meaning cached data is considered stale immediately after it is fetched. Whenever a component that calls a query mounts (or re-mounts), React Query schedules a background refetch, even if the same data was fetched milliseconds ago by a sibling component.
On dashboard pages that mount many components in parallel — each calling their own tRPC hooks — this default behaviour generates a burst of redundant HTTP requests on every navigation.
What Changed
A global default staleTime of 30,000 ms (30 seconds) is now set on the shared QueryClient:
// src/lib/trpc/provider.tsx
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
},
},
})
This means that if a query's data was fetched within the last 30 seconds, React Query will serve it from the cache without issuing a new network request — regardless of how many components mount and call the same query.
Choosing the Right staleTime Per Query
The 30-second default is appropriate for most application data. However, some data is effectively static between deployments and can be cached indefinitely:
| Query | Recommended staleTime | Reason |
|---|---|---|
billing.getPlans | Infinity | Plan catalogue changes only on deployment |
wearTear.list | Infinity | Reference data; curated, not user-driven |
complianceHelp.list | Infinity | Compliance rules are deployment-time constants |
| User session / tenancy data | 30_000 (default) | Changes during a session; use the global default |
| Inspection evidence / photos | 0 or short TTL | Frequently mutated; keep fresh |
To apply a per-query override:
// Treat plan data as permanently fresh until the page is reloaded
const { data: plans } = trpc.billing.getPlans.useQuery(undefined, {
staleTime: Infinity,
});
Why This Matters
- Fewer network round-trips. Dashboard pages with 10+ concurrent queries previously fired up to 10 background requests on every mount. With a 30-second window, subsequent mounts within that window are served from cache.
- Lower server load. Read-heavy reference data endpoints no longer receive redundant hits from UI re-renders or navigation back to a previously visited page.
- Predictable behaviour. Developers can now reason about cache lifetime explicitly rather than relying on the opaque "always refetch" default.
Trade-offs and Considerations
- Staleness window. Data mutated by another session (e.g. a landlord updating a category on a separate device) will not be reflected until the cache entry expires (30 seconds) or is explicitly invalidated via
queryClient.invalidateQueries. - Mutation-driven invalidation. Any tRPC mutation that changes data should continue to call
utils.<procedure>.invalidate()after success. ThestaleTimechange does not affect invalidation — invalidated queries are always refetched regardless of staleness. InfinitystaleTime queries. Queries set tostaleTime: Infinitywill never refetch in the background. Only use this for data that is guaranteed not to change within a user session (e.g. static reference lists).