Skip to main content

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

QueryManagerQueryOperations
Full CRUD (list, retrieve, create, update, delete)
Infinite scroll / pagination
Optimistic updates with auto-rollbackmanual
Automatic cache sync between list and detail
Custom one-off querieslimited
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 useQuery and useMutation
  • outside React components -> prefer direct queries and mutations

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:

  1. read or coordinate with queryKeys
  2. 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:

  1. Complete Documentation - Dive deep into all features and APIs
  2. Optimistic Updates - Learn how to implement instant UI feedback
  3. Cache Management - Master advanced caching strategies
  4. Error Handling - Implement robust error handling patterns
  5. 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