Appearance
Queries & Mutations
Queries and mutations are the single source of truth for fetching, caching, and mutating server data. They provide a declarative and predictable way to manage async state with built-in support for caching, stale data management, background refetching, and request deduplication.
By centralizing all query and mutation definitions under src/queries/, we ensure:
- Consistent naming across the app.
- Predictable cache keys.
- Clear separation of concerns between data fetching, UI logic, and state management.
Note: Unlike Pinia stores (for client-side app state), queries are strictly for server state — data that lives on the backend and can be invalidated or refetched at any time.
Location & File Naming
- All query and mutation definitions must live under:
src/queries/ - File names use camelCase and end with the suffix
.query.ts.- ✅
products.query.ts - ✅
shoppingCart.query.ts - ❌
productsQuery.ts,Products.query.ts
- ✅
Export Naming
- Queries:
- Exported as
use<Entity>Queryoruse<Entity><Variant>Query. - ✅
useProductsQuery,useProductDetailQuery,useProductsInfiniteQuery
- Exported as
- Mutations:
- Exported as
use<Entity><Action>Mutation. - ✅
useCreateOrderMutation,useUpdateProductMutation
- Exported as
- Always use named exports. Never
export default. - Names must reflect their domain and purpose.
- ❌
useFetchData,useMutateThing
- ❌
Queries
- Queries must wrap API functions from
src/services/api/<entity>.ts. - Query function name follow the
use<Entity><Action>Querypattern (useOrderListQuery). - Queries and the related mutations for a domain live in the same file (
src/queries/productLines.query.ts) to keep ownership clear. - Keys must be defined via a single key factory per file (
<entity>Keys) with structured arrays (['module', 'entity', params] as const). Never import key factories across domains. - Query-key argument interfaces end with
QueryKeyArgs. - Hooks accept a single Args object merging variables +
Partial<UseQueryOptions<...>>and should default that object to{}wherever possible souseArticlesQuery()is valid out of the box. - Variables use
MaybeRefOrGetterto support refs, getters, or literals. - Destructure args into named variables plus
...optionsfor configuration. - Never build query keys inline—always use the factory.
- Expose the typed
query.errorref and translate it into user-facing messaging via a computed helper rather than inline template logic.
Argument Naming
- If the query accepts multiple properties bundled into an object, name the property
filtersto signal it represents request parameters passed to the query function or API call, and to align with the rest of the conventions. - If the query accepts a single property, expose it directly by its semantic name (
id,slug,email, etc.) instead of wrapping it underparams.
Disabling/Pausing Queries
- Do not hardcode the
enabledoption inside a query hook. - The
enabledflag defines when a query should run, and those conditions usually depend on the consuming component (e.g., waiting for anidto exist, a form to be filled, or a route param to be available). - Keep the hook itself generic and always assume arguments are valid once the query is called.
- The component that uses the query is responsible for controlling whether it should execute by passing
enabledexplicitly.
Mutations
Location & Naming
- Mutations live in the same
src/queries/<entity>.query.tsfile as related queries. - Mutation function name follow the
use<Entity><Action>Mutationpattern (useCreateOrderMutation). - Always exported as named exports; never
default.
Args & Typing
- Accept a single Args object merging required variables with
Partial<UseMutationOptions<...>>. - Args interfaces end with
MutationArgs(e.g.,UseCreateOrderMutationArgs). - Support
MaybeRefOrGetterinputs for flexibility.
Cache Interaction
Shared invalidation logic should be extracted into helpers: when multiple mutations (create/update/delete) touch the same cached data, copy-pasting invalidation/refeching query methods, or complex predicate functions in every mutation quickly gets brittle. Centralize that behavior in a small helper so cache rules live in one place.
Mutations must update the cache after success.
Usage in Components
- Instantiate queries and mutations once per component setup.
- Queries:
const productsQuery = useProductsQuery({ filters }) - Mutations:
const createOrderMutation = useCreateOrderMutation()
- Queries:
- Always assign the result to a constant that mirrors the hook name without the
useprefix (productsQuery,createOrderMutation). - Do not destructure the query or mutation object at the top level. Always keep the instance intact (e.g.,
productsQuery,createOrderMutation) and access members through the object returned.- This preserves reactivity, keeps the origin of each property obvious, and ensures future fields added by the query/mutation instance remain immediately available without refactoring.
- For loading state, prefer using methods/computed that informs that the query or mutation is in progress.
- Wrap user actions in small handlers that call the mutation action async inside
try/catch, so UI feedback and side effects remain in the component. - Expose user-friendly error messages through computed properties so templates don’t scatter error-handling logic.
Error & Success Handling
- Reserve mutation option callbacks for non-visual side effects such as logging, cache updates, or resetting internal state. User-facing feedback (e.g., showing a toast or redirecting) should happen directly after the mutateAsync call inside a try/catch block. This keeps the success/error flow explicit in the component and avoids scattering UI logic across mutation options.
- Run cache updates first on success callback when defining the mutation, then delegate to consumer callbacks.
- Components use the mutation async method wrapped in
try/catchfor user-facing feedback.
Rationale
- Enforcing
use-prefixed hooks and structured query keys makes queries and mutations predictable and easy to discover. - Housing each entity’s queries and mutations in the same file, with a single key factory per domain, keeps ownership tight and prevents cross-file drift.
- Lifecycle and cache interaction rules ensure hooks remain import-safe, predictable, and integrate cleanly with the service layer.
- Shared invalidation helpers prevent drift, reduce duplication, and keep cache behavior consistent across mutations.
- Defaulted Args objects (
useArticlesQuery({})) lower the friction for consumers while still allowing overrides via...options. - Centralizing all queries/mutations in
src/queries/separates server state from UI logic and keeps ownership clear. - Guarding query data behind fetch/error checks and exposing typed error refs encourages components to serve safe defaults and user-friendly messaging through computed helpers.
- Prefer optimistic updates when the returned entity can be deterministically merged into cache; otherwise invalidate to prevent inconsistent state.
- Avoiding inline mutations and reserving success/error callbacks for non-visual side effects ensures UI feedback stays in the component, where it is easiest to trace and maintain.
- Using named exports (never default) and aligning with domain naming conventions keeps imports explicit and discoverable.
Guides
Library-specific implementation guide: