Skip to content

feat(COMPT-41): implement usePaginatedQuery offset and cursor modes#4

Merged
a-elkhiraooui-ciscode merged 1 commit intodevelopfrom
feat/COMPT-41-use-paginated-query
Apr 7, 2026
Merged

feat(COMPT-41): implement usePaginatedQuery offset and cursor modes#4
a-elkhiraooui-ciscode merged 1 commit intodevelopfrom
feat/COMPT-41-use-paginated-query

Conversation

@a-elkhiraooui-ciscode
Copy link
Copy Markdown

  • usePaginatedQuery(queryDef, params, options) supports mode: 'offset' | 'cursor'
  • Offset mode: page/pageSize (default 20)/nextPage/prevPage/totalPages
  • Cursor mode: fetchNextPage/hasNextPage/nextCursor via useInfiniteQuery
  • Both expose data as flat T[] array, isLoading, isFetching, isError, error
  • Offset uses useQuery with page in queryKey; cursor uses useInfiniteQuery
  • getCursor option required for cursor mode
  • Typed overloads: full inference, no TanStack internals exposed
  • 15 tests, 100% coverage on usePaginatedQuery.ts, 95.62% overall

Closes COMPT-41

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

- usePaginatedQuery(queryDef, params, options) supports mode: 'offset' | 'cursor'
- Offset mode: page/pageSize (default 20)/nextPage/prevPage/totalPages
- Cursor mode: fetchNextPage/hasNextPage/nextCursor via useInfiniteQuery
- Both expose data as flat T[] array, isLoading, isFetching, isError, error
- Offset uses useQuery with page in queryKey; cursor uses useInfiniteQuery
- getCursor option required for cursor mode
- Typed overloads: full inference, no TanStack internals exposed
- 15 tests, 100% coverage on usePaginatedQuery.ts, 95.62% overall

Closes COMPT-41
@a-elkhiraooui-ciscode a-elkhiraooui-ciscode requested a review from a team as a code owner April 7, 2026 11:00
Copilot AI review requested due to automatic review settings April 7, 2026 11:00
@a-elkhiraooui-ciscode a-elkhiraooui-ciscode merged commit 71440e7 into develop Apr 7, 2026
3 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new usePaginatedQuery hook to the query utilities, providing a unified pagination API that supports both offset- and cursor-based pagination on top of TanStack Query.

Changes:

  • Introduces usePaginatedQuery with overloads for mode: 'offset' | 'cursor', returning flattened data plus pagination helpers.
  • Implements offset pagination via useQuery and cursor pagination via useInfiniteQuery.
  • Adds a comprehensive test suite for the new hook and re-exports it from the query barrel.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/query/usePaginatedQuery.ts Implements the new pagination hook with offset and cursor modes and typed result shapes.
src/query/usePaginatedQuery.test.tsx Adds unit tests covering both modes and common pagination behaviors.
src/query/index.ts Re-exports usePaginatedQuery so it becomes part of the package’s public API via src/index.ts.

Comment on lines +84 to +92
const pageSize = isOffset ? ((options as OffsetPaginationOptions).pageSize ?? 20) : 20;
const [page, setPage] = React.useState(
isOffset ? ((options as OffsetPaginationOptions).initialPage ?? 1) : 1,
);

// --- Offset: useQuery ----------------------------------------------------
const offsetQuery = useTanstackQuery<TData[], Error>({
queryKey: [...queryDef.queryKey({ ...params, page, pageSize } as TParams), page, pageSize],
queryFn: () => queryDef.queryFn({ ...params, page, pageSize } as TParams),
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

params is typed as unconstrained TParams, but the implementation relies on object spread ({ ...params, ... }) and then casts back to TParams. This can break at runtime (e.g., if a query definition uses a primitive param like string/number, queryFn will receive an object instead) and also undermines type safety. Consider constraining TParams to an object type (e.g. TParams extends Record<string, unknown>) and/or splitting “base params” from pagination params in the overloads so queryDef can be typed to accept TBaseParams & { page/pageSize | cursor } without casts.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +92
const offsetQuery = useTanstackQuery<TData[], Error>({
queryKey: [...queryDef.queryKey({ ...params, page, pageSize } as TParams), page, pageSize],
queryFn: () => queryDef.queryFn({ ...params, page, pageSize } as TParams),
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The offset queryKey appends page/pageSize after already calling queryDef.queryKey with merged { ...params, page, pageSize }. If the queryDef key already includes these fields (as in the tests), the key will contain duplicates; more importantly, it prevents cache sharing with queryDef.useQuery(mergedParams) because the keys no longer match. Prefer using exactly queryDef.queryKey(mergedParams) (or alternatively queryDef.queryKey(params) + page/pageSize once) to keep key composition consistent.

Suggested change
const offsetQuery = useTanstackQuery<TData[], Error>({
queryKey: [...queryDef.queryKey({ ...params, page, pageSize } as TParams), page, pageSize],
queryFn: () => queryDef.queryFn({ ...params, page, pageSize } as TParams),
const offsetParams = { ...params, page, pageSize } as TParams;
const offsetQuery = useTanstackQuery<TData[], Error>({
queryKey: queryDef.queryKey(offsetParams),
queryFn: () => queryDef.queryFn(offsetParams),

Copilot uses AI. Check for mistakes.
error: offsetQuery.error,
page,
pageSize,
totalPages: undefined,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totalPages is part of the offset result but is currently always returned as undefined. This makes the API misleading (and appears to conflict with the PR description). Either implement a way to compute/provide totalPages (e.g., via an option or by supporting a query result shape that includes total count) or remove/rename the field so consumers don’t rely on a value that can never be present.

Suggested change
totalPages: undefined,

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +149
fetchNextPage: () => infiniteQuery.fetchNextPage(),
hasNextPage: infiniteQuery.hasNextPage,
nextCursor,
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchNextPage is exposed as () => void, but it actually returns a Promise from TanStack Query. Typing it as void prevents consumers from awaiting pagination (useful for UI flows and tests). Consider exposing it as () => Promise<unknown> (or a small custom promise type) to keep TanStack internals hidden while still allowing await fetchNextPage().

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants