Skip to main content

Pagination

Query Cache Flow supports paginated queries by including query parameters in cache keys. This ensures each page of data has its own cache entry.

The Problem​

Without proper key management, pagination causes cache conflicts:

// Bad: Same key for all pages
const { data: page1 } = useQuery({ queryKey: ['transactions'] });
const { data: page2 } = useQuery({ queryKey: ['transactions'] }); // Overwrites page1!

The Solution: Query Parameters in Keys​

Include pagination parameters in your query keys:

// Good: Unique key per page
const { data: page1 } = useQuery({
queryKey: [{ entity: 'transactions', method: 'list', query: { page: 1 } }]
});
const { data: page2 } = useQuery({
queryKey: [{ entity: 'transactions', method: 'list', query: { page: 2 } }]
});

Extending QueryGroup for Pagination​

Override the list operation to accept query parameters:

import { createQueryGroupCRUD, inyectKeysToQueries } from 'src/queries';
import { RemindersQueryParams } from 'src/generated';

let remindersQueryGroupCRUD = createQueryGroupCRUD<string>('reminders');
remindersQueryGroupCRUD = inyectKeysToQueries(remindersQueryGroupCRUD, {
auth: true,
});

export const remindersQueryGroup = {
...remindersQueryGroupCRUD,

// Override list to accept query params
list: {
queryKey: (query?: RemindersQueryParams) => ({
...remindersQueryGroupCRUD.list.queryKey,
query, // Adds: { page, limit, sort, filter, etc. }
}),
},
};

Query Parameter Types​

Define your query parameters based on your API:

interface RemindersQueryParams {
page?: number;
limit?: number;
sortBy?: 'createdAt' | 'dueDate' | 'title';
sortOrder?: 'asc' | 'desc';
status?: 'pending' | 'completed' | 'overdue';
accountId?: string;
}

Using Paginated Queries​

Basic Pagination Hook​

export const useReminders = (params?: RemindersQueryParams) =>
generatedReminders({
query: {
queryKey: [remindersQueryGroup.list.queryKey(params)],
},
// Pass params to the API call
...params,
});

In Components​

function RemindersList() {
const [page, setPage] = useState(1);
const [filters, setFilters] = useState<RemindersQueryParams>({
limit: 20,
status: 'pending',
});

const { data, isLoading } = useReminders({
...filters,
page,
});

return (
<>
<ReminderFilters value={filters} onChange={setFilters} />
<ReminderList items={data?.items} />
<Pagination
page={page}
totalPages={data?.totalPages}
onPageChange={setPage}
/>
</>
);
}

Invalidation Strategies​

Invalidate All Pages​

Use the base list key without parameters to invalidate all paginated queries:

// Invalidates ALL reminder pages
queryClient.invalidateQueries({
queryKey: [{ entity: 'reminders', method: 'list' }],
});

// Or use the all key for even broader invalidation
queryClient.invalidateQueries({
queryKey: [remindersQueryGroup.all.queryKey],
});

Invalidate Specific Page​

// Invalidates only page 1
queryClient.invalidateQueries({
queryKey: [remindersQueryGroup.list.queryKey({ page: 1 })],
});

Mutation Invalidation​

When creating/updating/deleting, invalidate the list (all pages):

export const useReminderCreate = ({ onSuccess, ...rest }) =>
generatedReminderCreate({
mutation: {
onSuccess: (data, variables, context) => {
// Invalidate all pages - new item could appear on any page
invalidateQueriesForKeys([
{ entity: 'reminders', method: 'list' },
]);
onSuccess?.(data, variables, context);
},
...rest,
},
});

Cursor-Based Pagination​

For cursor/infinite scroll pagination:

interface MovementsQueryParams {
cursor?: string;
limit?: number;
accountId?: string;
}

export const movementsQueryGroup = {
all: {
queryKey: { entity: 'movements' },
},
list: {
queryKey: (query?: MovementsQueryParams) => ({
entity: 'movements',
method: 'list',
query,
}),
},
};

With Infinite Query​

export const useMovementsInfinite = (params: Omit<MovementsQueryParams, 'cursor'>) =>
useInfiniteQuery({
queryKey: [movementsQueryGroup.list.queryKey(params)],
queryFn: ({ pageParam }) =>
fetchMovements({ ...params, cursor: pageParam }),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});

Complex Filtering​

For APIs with many filter options:

interface TransactionsQueryParams {
// Pagination
page?: number;
limit?: number;

// Sorting
sortBy?: 'date' | 'amount' | 'category';
sortOrder?: 'asc' | 'desc';

// Filters
accountId?: string;
categoryId?: string;
startDate?: string;
endDate?: string;
minAmount?: number;
maxAmount?: number;
type?: 'income' | 'expense';
search?: string;
}

export const transactionsQueryGroup = {
...transactionsQueryGroupCRUD,

list: {
queryKey: (query?: TransactionsQueryParams) => ({
...transactionsQueryGroupCRUD.list.queryKey,
query,
}),
},

// Helper for filtering without pagination
filtered: {
queryKey: (filters: Omit<TransactionsQueryParams, 'page' | 'limit'>) => ({
entity: 'transactions',
method: 'list',
query: filters,
}),
},
};

Prefetching Pages​

Improve UX by prefetching adjacent pages:

function TransactionsList() {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();

// Prefetch next page
useEffect(() => {
queryClient.prefetchQuery({
queryKey: [transactionsQueryGroup.list.queryKey({ page: page + 1 })],
queryFn: () => fetchTransactions({ page: page + 1 }),
});
}, [page]);

// ...
}

Best Practices​

1. Always Include Query Params in Keys​

// Good
queryKey: [queryGroup.list.queryKey({ page, filters })]

// Bad - will cause cache conflicts
queryKey: [queryGroup.list.queryKey]

2. Use Partial Matching for Invalidation​

// Invalidates all variations of the list query
queryClient.invalidateQueries({
queryKey: [{ entity: 'transactions', method: 'list' }],
exact: false, // Default - matches any query starting with this key
});

3. Keep Query Params Serializable​

// Good - serializable
{ page: 1, startDate: '2024-01-01' }

// Bad - non-serializable values
{ page: 1, dateRange: new DateRange() }

4. Normalize Filter Values​

Ensure consistent cache keys by normalizing undefined/empty values:

list: {
queryKey: (query?: QueryParams) => ({
...baseCRUD.list.queryKey,
query: query ? normalizeParams(query) : undefined,
}),
}

function normalizeParams(params: QueryParams): QueryParams {
return Object.fromEntries(
Object.entries(params).filter(([_, v]) => v !== undefined && v !== '')
);
}

Summary​

PatternUse Case
Page-basedTraditional pagination with page numbers
Cursor-basedInfinite scroll, real-time feeds
Filter-basedSearch, filtering, sorting
CombinedComplex data grids with all features

Query Cache Flow's query parameter support makes all pagination patterns work seamlessly with automatic cache management.