React-Query for decentralised state management
Use React Query's request deduplication and cache invalidation to eliminate shared state — components fetch independently and stay in sync through query invalidation, not props or context.
Install in BlintOpens the Blint desktop app and adds this pattern to your project. Don't have Blint?
Why this matters
React Query deduplicates in-flight requests and shares cached responses across every component that uses the same query key. This means two sibling components calling the same hook produce one network request, not two. Leveraging this eliminates the need for lifted state, shared context, or prop drilling just to keep siblings in sync.
The result is lower coupling: each widget owns its own data access, mutations invalidate the cache, and React Query propagates fresh data to every subscriber automatically.
Core rules
Components must fetch their own data
- A component that needs server data must call the appropriate hook directly, not receive the data as a prop from a parent.
- Parents should not fetch on behalf of children and pass results down. Each component is responsible for its own query.
- This applies equally to list views, detail views, and form sections within an editor.
Why: React Query deduplicates identical requests. Fetching independently costs nothing extra at the network level but decouples components from each other.
Editor screens: each section owns its slice
When a page presents multiple widgets or sections that edit parts of the same model:
- Each section should call the model hook independently (e.g.
useSite(id)). - Each section should use its own mutation hook to push changes for its slice.
- Sections must not coordinate through a shared parent form state or lifted state.
- After a successful mutation, invalidate the relevant query keys so sibling sections receive the updated data automatically.
Mutations must invalidate, not broadcast
- On create: invalidate all list queries for that model (
queryClient.invalidateQueries({ queryKey: [modelKey] })). - On update: set the specific item cache immediately for responsiveness, then invalidate list queries so filtered/sorted views refresh.
- On delete: remove the specific item from cache, then invalidate list queries.
- When mutating a related resource (e.g. a site's external references), invalidate both the related resource query and the parent entity query.
Why: Invalidation lets React Query decide when to refetch based on whether any component is actually subscribed. Broadcasting state changes manually reimplements what the cache already does.
Query keys must be hierarchical and deterministic
- Structure keys as
[modelName, id?, filters?]so that wildcard invalidation (exact: false) naturally cascades from broad to narrow. - Related sub-resources should nest under the parent:
[modelName, parentId, subResource]. - Never construct query keys with unstable references (new objects on every render). Use stable primitives or memoised filter objects.
Why: Hierarchical keys enable targeted invalidation — invalidating ["sites"] hits every sites query, while ["sites", id] hits only that item.
Do not introduce shared state to avoid refetching
- Never add React context, Zustand, Redux, or lifted parent state as a workaround for "too many queries".
- If multiple components need the same data, they should call the same hook. React Query handles deduplication and caching.
- If stale data is acceptable for a period, configure
staleTimeon the query rather than caching manually.
Keep hooks as the single data-access interface
- All server data access should go through model hooks, never raw
fetchoraxioscalls in components. - Hooks encapsulate query keys, fetch functions, pagination, and validation — components should not know these details.
- Standard CRUD models should share a hook factory that stamps out list/detail/mutation hooks with consistent query-key structure, invalidation, and pagination — that consistency is what makes wildcard invalidation reliable.