Getting Started
@codeleap/query is the CodeLeap server-state layer for apps that already use TanStack Query.
Use this package when the app needs to standardize:
- resource lists
- detail queries
- create, update, and delete flows
- action-style mutations that should coordinate with cache
- invalidation, prefetching, and deliberate cache updates
Do not treat it as a replacement for TanStack Query. It builds on TanStack Query and gives apps a more opinionated, reusable structure.
Introduction
A powerful TypeScript library that provides type-safe CRUD operations with optimistic updates, infinite scroll pagination, and intelligent cache management built on top of React Query. Designed for modern React applications that need robust data management with excellent developer experience.
Important: This library is built on TanStack Query v5. You should have a solid understanding of React Query concepts like queries, mutations, cache management, and query keys before using this library. If you're new to React Query, we recommend reading the TanStack Query documentation first.
This library provides two main approaches for managing data operations:
- QueryManager: Full-featured CRUD operations with infinite scroll, optimistic updates, and automatic cache synchronization
- QueryOperations: Lightweight, type-safe query and mutation builder with fluent API
Choose the right abstraction first
This is the main decision you should make before writing code.
Use QueryManager when
- the feature is a resource with identity
- the app has list and detail views for the same resource
- you need create, update, or delete flows
- optimistic CRUD behavior matters
- list and detail cache should stay synchronized automatically
Use QueryOperations when
- the task is not a full CRUD resource
- you need custom typed queries
- you need action-style mutations beside an existing resource
- the endpoint is better described as an operation than as a resource manager
Common mistake
Do not force action-style endpoints into a separate CRUD manager just because they talk to the backend.
If the app already has a manager for a resource, extra actions often belong in QueryOperations while cache coordination still uses the manager's helpers.
Key Features
- Type Safety: Full TypeScript support with automatic type inference for all operations
- Optimistic Updates: Instant UI updates with automatic rollback on errors
- Infinite Scroll: Built-in pagination support with seamless data loading
- Smart Caching: Intelligent cache synchronization between list and individual queries
- Dual Architecture: Choose between full-featured QueryManager or lightweight QueryOperations
- Zero Configuration: Works out of the box with sensible defaults
When to Use This Library
This library is ideal for applications that need:
- Complex data relationships with lists and individual items
- Real-time feel with optimistic updates
- Large datasets requiring pagination
- Strong type safety throughout the data layer
- Consistent caching across multiple components
TanStack Query Knowledge Required
You should be familiar with these TanStack Query concepts:
- Queries: How to fetch and cache data with
useQuery - Mutations: How to modify data with
useMutation - Query Keys: Understanding how React Query identifies and caches queries
- Cache Management: Invalidation, refetching, and optimistic updates
- Error Handling: How React Query handles and retries failed requests
New to TanStack Query? Read the official documentation and complete their tutorial before proceeding.
Installation
Install the library using your preferred package manager:
# Using npm
npm install @codeleap/query @tanstack/react-query
# Using yarn
yarn add @codeleap/query @tanstack/react-query
# Using pnpm
pnpm add @codeleap/query @tanstack/react-query
# Using bun
bun add @codeleap/query @tanstack/react-query
How This Library Extends TanStack Query
This library doesn't replace TanStack Query - it builds upon it by providing:
What TanStack Query Provides
- Core query and mutation functionality
- Caching, background updates, and synchronization
- Devtools and debugging capabilities
- Error handling and retry mechanisms
What This Library Adds
- Structured CRUD patterns with consistent APIs
- Automatic cache synchronization between list and detail views
- Optimistic updates with built-in rollback mechanisms
- Infinite scroll pagination with zero configuration
- Type-safe operation builders with automatic inference
- Advanced cache mutations for complex UI updates
Working Together
// You can still use TanStack Query directly alongside this library
import { useQuery } from '@tanstack/react-query'
import { createQueryManager } from '@codeleap/query'
// Direct TanStack Query usage
const customQuery = useQuery({
queryKey: ['custom-data'],
queryFn: fetchCustomData
})
// This library's enhanced patterns
const userManager = createQueryManager({
name: 'users',
// ... configuration
})
QueryManager vs QueryOperations — Which to use
| QueryManager | QueryOperations | |
|---|---|---|
| Full CRUD (list, retrieve, create, update, delete) | ✅ | ❌ |
| Infinite scroll / pagination | ✅ | ❌ |
| Optimistic updates with auto-rollback | ✅ | manual |
| Automatic cache sync between list and detail | ✅ | ❌ |
| Custom one-off queries | limited | ✅ |
| Fluent, chainable API | ❌ | ✅ |
| Lightweight / no boilerplate | ❌ | ✅ |
Use QueryManager when you have a resource with a standard REST CRUD pattern (users, posts, orders). It handles everything automatically.
Use QueryOperations when you need queries that don't fit a resource model — analytics endpoints, action mutations (e.g. markAsRead), or any query that doesn't map to a list/retrieve/create/update/delete pattern.
// QueryManager — for a full resource
const usersManager = createQueryManager<User>({ name: 'users', ... })
const { items } = usersManager.useList()
const create = usersManager.useCreate({ optimistic: true })
// QueryOperations — for custom endpoints
const ops = createQueryOperations({ queryClient })
.query('dashboard', () => api.get('/analytics/dashboard'))
.mutation('markAsRead', (id: string) => api.post(`/notifications/${id}/read`))
Hooks vs direct functions
This distinction matters mainly for QueryOperations.
- inside React components -> prefer hooks like
useQueryanduseMutation - outside React components -> prefer direct
queriesandmutations
For QueryManager, React components should usually use manager hooks such as useList, useRetrieve, useCreate, useUpdate, and useDelete.
Do not call hooks in services, utility modules, event pipelines, or any non-React execution context.
queryKeys vs mutations
This is one of the most common points of confusion.
Use queryKeys when the job is to coordinate queries:
- invalidate
- refetch
- cancel
- prefetch
- inspect cache
- remove query data
Use mutations when the job is to edit cached resource data:
- add items
- remove items
- update items
- restore optimistic positions
It is normal to use both in the same flow:
- read or coordinate with
queryKeys - patch cached resource data with
mutations
Invalidate vs direct cache mutation
Do not default to invalidation for every mutation.
Prefer direct cache mutation when:
- you already know how the cached resource item changed
- the UI should reflect the result immediately
- the package already provides the right manager mutation helper
Prefer invalidation when:
- the server is the easiest source of truth for the resulting state
- multiple derived queries may need refreshing
- you cannot confidently patch the affected cached shape
In many real flows, you will combine both:
- patch the visible cached item immediately
- invalidate related or derived queries afterward if needed
Quick Setup
1. Configure React Query
First, set up React Query in your application:
// main.tsx or App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
Shared query client rule
Use one shared application query client.
- create it once at app setup
- provide it once at app root
- reuse it in managers and operations
- do not create feature-local query clients
If your app uses QueryClientEnhanced, that should still wrap the same shared client instead of creating a second state layer.
2. Define Your Data Types
Create TypeScript interfaces that extend QueryItem:
// types/user.ts
import { QueryItem } from '@codeleap/query'
export interface User extends QueryItem {
id: string
name: string
email: string
status: 'active' | 'inactive'
role: 'admin' | 'user'
createdAt: string
updatedAt: string
}
export interface UserFilters {
status?: 'active' | 'inactive'
role?: 'admin' | 'user'
search?: string
}
Create your QueryManager
Perfect for full CRUD applications with lists and individual items:
import { createQueryManager } from '@codeleap/query'
import { userApi } from '../api/users'
import { User, UserFilters } from '../types/user'
export const usersManager = createQueryManager<Thread>({
name: 'users',
queryClient: queryClient.client,
async listFn(limit, offset, filters) {
const response = await fetch(`/api/users?${new URLSearchParams([limit, offset, filters])}`)
if (!response.ok) throw new Error('Failed to fetch users')
return response.json()
},
async retrieveFn(id) {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Failed to fetch user')
return response.json()
},
async createFn(data) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('Failed to create user')
return response.json()
},
async updateFn(data) {
const response = await fetch(`/api/users/${data?.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!response.ok) throw new Error('Failed to update user')
return response.json()
},
async deleteFn(id) {
const response = await fetch(`/api/users/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete user')
return id
},
})
Create your QueryOperations (For custom query patterns)
Ideal for custom query patterns with full type safety:
import { createQueryOperations } from '@codeleap/query'
import { userApi } from '../api/users'
export const userOperations = createQueryOperations({ queryClient })
.mutation('readUser', async (to) => {
await fetch(`/api/user/`, { method: 'POST', body: JSON.stringify({ read: to }) })
})
.query('listProperties', async () => {
const response = await fetch(`/api/user/properties`)
return response.json()
})
Your First Component
Now you can use the library in your components:
// components/UserList.tsx
import React from 'react'
import { userQueryManager } from '../hooks/useUserManager'
export function UserList() {
// Get paginated list with infinite scroll
const { items: users, query } = userQueryManager.useList({
filters: { status: 'active' },
limit: 20,
})
// Create mutation with optimistic updates
const createMutation = userQueryManager.useCreate({
optimistic: true,
appendTo: 'start',
listFilters: { status: 'active' },
})
const handleCreateUser = () => {
createMutation.mutate({
name: 'New User',
email: 'user@example.com',
status: 'active',
role: 'user',
})
}
if (query.isLoading) return <div>Loading users...</div>
if (query.error) return <div>Error: {query.error.message}</div>
return (
<div>
<button onClick={handleCreateUser} disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating...' : 'Add User'}
</button>
<div className="user-list">
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
{query.hasNextPage && (
<button
onClick={() => query.fetchNextPage()}
disabled={query.isFetchingNextPage}
>
{query.isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}
What's Next?
Now that you have the basics set up, you can explore more advanced features:
- Complete Documentation - Dive deep into all features and APIs
- Optimistic Updates - Learn how to implement instant UI feedback
- Cache Management - Master advanced caching strategies
- Error Handling - Implement robust error handling patterns
- Performance Optimization - Tips for handling large datasets efficiently
Quick Examples
Infinite Scroll List
const { items, query } = userQueryManager.useList({ limit: 50 })
// Automatically handles pagination, loading states, and errors
Optimistic Create
const createMutation = userQueryManager.useCreate({
optimistic: true,
appendTo: 'start'
})
// UI updates immediately, rolls back on error
Smart Caching
const { item } = userQueryManager.useRetrieve(userId)
// Uses list cache as initial data, syncs both ways
Type-Safe Operations
const userQuery = userOperations.useQuery('getUser', userId)
// Full TypeScript inference for parameters and return types
Need Help?
- Check the complete documentation for detailed examples
- Look at the TypeScript types for available options
- Use React Query DevTools to inspect cache state
- Consider the library's internal architecture for advanced use cases