Reducing Unnecessary Data Refetches with QueryClient staleTime (PERF-06)
Reducing Unnecessary Data Refetches with QueryClient staleTime (PERF-06)
Version introduced: v1.0.83
Category: Performance · Frontend
File affected: src/lib/trpc/provider.tsx
Background
The platform dashboard loads data for multiple domains simultaneously — developments, compliance obligations, service charges, owner records, and more. Each of these domains maps to one or more tRPC queries executed by React components.
Prior to v1.0.83, TRPCProvider initialised QueryClient with no options:
const queryClient = new QueryClient();
React Query's default staleTime is 0ms, which means cached data is considered stale the instant it arrives. Every time a component mounts — including on routine tab or route navigation — React Query schedules a background refetch, even if the data was fetched moments ago.
For a dashboard with many concurrent queries this produces a waterfall of redundant network requests on every navigation event.
The Fix
QueryClient is now initialised with sensible global defaults in TRPCProvider:
// src/lib/trpc/provider.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // data stays fresh for 1 minute
gcTime: 5 * 60 * 1000, // inactive cache entries live for 5 minutes
},
},
});
| Option | Value | Effect |
|---|---|---|
staleTime | 60 000 ms (1 min) | Queries will not refetch on mount if data was loaded within the last minute |
gcTime | 300 000 ms (5 min) | Cached data is retained in memory for 5 minutes after the last subscriber unmounts |
Fine-Tuning for Reference Data
Some data sets change very infrequently — the developments list, compliance obligation templates, and unit type configurations are good examples. These can be given a longer staleTime of 5 minutes either globally via setQueryDefaults or per-query:
Using setQueryDefaults
// After creating queryClient, before passing it to TRPCProvider
queryClient.setQueryDefaults(
['developments', 'list'],
{ staleTime: 5 * 60 * 1000 }
);
queryClient.setQueryDefaults(
['compliance', 'obligations'],
{ staleTime: 5 * 60 * 1000 }
);
Using a per-query option
const { data } = trpc.developments.list.useQuery(undefined, {
staleTime: 5 * 60 * 1000,
});
Expected Behaviour After This Change
- Tab navigation: Switching between dashboard sections (e.g. Compliance → Service Charges → Developments) will no longer trigger refetches for queries that were loaded within the last minute.
- Component remounts: Queries fetched during the current navigation session are reused from cache until
staleTimeexpires. - Background refresh: React Query still silently refetches in the background once
staleTimeelapses, keeping data up to date without blocking the UI. - Cache eviction: Unused query data is removed from memory after 5 minutes, preventing unbounded cache growth.
Trade-offs
| Concern | Mitigation |
|---|---|
| Users may see data that is up to 1 minute old | staleTime only prevents a background refetch, not a foreground one. Explicitly calling refetch() or invalidating the query still fetches fresh data immediately. |
| Mutations should invalidate affected queries | Use utils.invalidate() after any mutation that changes data covered by a cached query, as normal. |
| Real-time sensitive data | For queries that must always be current (e.g. payment status), override staleTime: 0 at the call site. |