Skip to content

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>Query or use<Entity><Variant>Query.
    • useProductsQuery, useProductDetailQuery, useProductsInfiniteQuery
  • Mutations:
    • Exported as use<Entity><Action>Mutation.
    • useCreateOrderMutation, useUpdateProductMutation
  • 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>Query pattern (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 so useArticlesQuery() is valid out of the box.
  • Variables use MaybeRefOrGetter to support refs, getters, or literals.
  • Destructure args into named variables plus ...options for configuration.
  • Never build query keys inline—always use the factory.
  • Expose the typed query.error ref 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 filters to 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 under params.

Disabling/Pausing Queries

  • Do not hardcode the enabled option inside a query hook.
  • The enabled flag defines when a query should run, and those conditions usually depend on the consuming component (e.g., waiting for an id to 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 enabled explicitly.

Mutations

Location & Naming

  • Mutations live in the same src/queries/<entity>.query.ts file as related queries.
  • Mutation function name follow the use<Entity><Action>Mutation pattern (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 MaybeRefOrGetter inputs 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()
  • Always assign the result to a constant that mirrors the hook name without the use prefix (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/catch for 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: