Skip to main content

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:

  • QueryManager owns the resource
  • QueryOperations adds 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.