How we fetch and mutate server data in the front-end with TanStack Query.
client/composables/useOpnApi.js
automatically injects the
Authorization: Bearer <token>
header on every request, and the global middleware
client/middleware/01.check-auth.global.js
pre-loads the current user & workspaces during navigation.client/stores/app.js
for an example.
list
) that you call to register the query. The options object you pass is forwarded directly to TanStack Query in case you need staleTime
, enabled
, etc.
mutateAsync
returns a Promise, so we recommend chaining handlers with .then().catch()
to keep control flow explicit:
/forms/{formId}/integrations/{integrationId}
), pass the required IDs when creating the mutation factory. This ensures proper cache invalidation and prevents stale closure issues:
mutateAsync()
calltoValue()
to unwrap reactive references (refs, computed) when building query keys or making API calls. This ensures the actual values are used rather than reactive objects.useMutation()
instances rather than calling them directlytoValue()
to extract values from refs/computed propertiesonSuccess
to avoid refetchingclient/composables/query/*
). After a successful API call they update the cache manually using queryClient.setQueryData
so the UI reflects changes instantly without needing an extra network round-trip.
If you need to invalidate and refetch, every composable exposes invalidate()
which calls queryClient.invalidateQueries
for its namespace:
useWorkspaces.js
as an example:
list()
, detail(id)
create()
, update()
, remove()
invalidate()
workspaceApi.*
, keeps its own queryKey
, and touches only the part of the cache it owns. This isolation makes cache behaviour predictable and prevents accidental over-fetching.
suspense()
suspense()
method that returns a Promise resolving once the data is loaded. Our auth middleware uses it to block navigation until the user and workspace lists are ready:
isLoading
, isFetching
, isError
. Mutations expose isPending
. Use them to drive spinners, disabled states, or retry logic.
isFetching && data
is perfect for subtle “background update” indicators.mutateAsync().then().catch()
.invalidate()
when you really need a refetch—otherwise rely on optimistic updates.