Overview
The QueryOperations class provides a lightweight, fluent API for building type-safe queries and mutations.
When QueryOperations is the right fit
Use QueryOperations when the task is not a full CRUD resource manager.
Good fits:
- action-style mutations
- custom dashboard or config queries
- feature-specific queries beside an existing manager
- typed imperative queries and mutations outside React components
Prefer QueryManager instead when the feature is clearly a resource with list, retrieve, create, update, and delete behavior.
Relationship with QueryManager
QueryOperations is not a replacement for QueryManager.
A common pattern is:
QueryManagerowns the resourceQueryOperationsadds extra queries or mutations beside it
In those flows, the operation executes the request while the manager often still helps with cache inspection or cache mutation.
Creating QueryOperations
import { createQueryOperations } from '@codeleap/query'
const userOperations = createQueryOperations({ queryClient })
.query('getUser', async (id: string) => {
const response = await api.get(`/users/${id}`)
return response.data
})
.query('getUsers', async (filters?: UserFilters) => {
const response = await api.get('/users', { params: filters })
return response.data
})
.mutation('createUser', async (data: Omit<User, 'id'>) => {
const response = await api.post('/users', data)
return response.data
})
.mutation('updateUser', async (data: Partial<User> & { id: string }) => {
const response = await api.put(`/users/${data.id}`, data)
return response.data
})
.mutation('deleteUser', async (id: string) => {
await api.delete(`/users/${id}`)
return id
})
Using QueryOperations Hooks
Queries
function UserProfile({ userId }: { userId: string }) {
// Type-safe query with automatic parameter and return type inference
const userQuery = userOperations.useQuery('getUser', userId, {
enabled: !!userId,
staleTime: 5 * 60 * 1000
})
const usersQuery = userOperations.useQuery('getUsers',
{ status: 'active' },
{
refetchInterval: 30 * 1000
}
)
if (userQuery.isLoading) return <div>Loading...</div>
if (userQuery.error) return <div>Error: {userQuery.error.message}</div>
return (
<div>
<h1>{userQuery.data?.name}</h1>
<p>Total active users: {usersQuery.data?.length}</p>
</div>
)
}
Mutations
function UserForm() {
const createMutation = userOperations.useMutation('createUser', {
onSuccess: (newUser) => {
// 'newUser' is automatically typed
toast.success(`Created user: ${newUser.name}`)
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ['getUsers'] })
}
})
const updateMutation = userOperations.useMutation('updateUser')
const deleteMutation = userOperations.useMutation('deleteUser')
const handleCreate = (userData: Omit<User, 'id'>) => {
// Parameters are type-checked
createMutation.mutate(userData)
}
return (
<form onSubmit={(e) => {
e.preventDefault()
handleCreate({
name: 'John Doe',
email: 'john@example.com',
status: 'active',
createdAt: new Date().toISOString()
})
}}>
{/* Form fields */}
<button
type="submit"
disabled={createMutation.isPending}
>
Create User
</button>
</form>
)
}
Prefetching and Cache Access
// Prefetch data
await userOperations.prefetchQuery('getUser', 'user-123', {
staleTime: 10 * 60 * 1000
})
// Get cached data
const cachedUser = await userOperations.getQueryData('getUser', 'user-123')
if (cachedUser) {
console.log('User already in cache:', cachedUser.name)
}
// Access operations
const { queries, mutations } = userOperations
console.log('Available queries:', Object.keys(queries))
console.log('Available mutations:', Object.keys(mutations))
Hooks vs direct functions
Use hooks when the code lives in React render flow and the UI needs query or mutation state.
Use direct queries and mutations functions when the code runs outside React components and only needs imperative async behavior.