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
Contributor

  • 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.
Zaiidmo added a commit that referenced this pull request Apr 10, 2026
* ops: updated sonar variable

* Feat/compt 40 create query factory (#3)

* feat(COMPT-40): implement createQuery factory

- Add createQuery(keyFn, fetcher) returning QueryDefinition<TParams, TData>
- TData and TParams fully inferred from fetcher signature, zero manual annotation
- queryKey returns stable readonly tuple via keyFn
- useQuery shorthand hook wraps useTanstackQuery with typed params
- Export from src/index.ts
- Add @tanstack/react-query as peerDependency (>=5) and devDependency
- Add pnpm cssstyle override to fix Node.js v22 + jsdom@28 ESM compat issue
- Fix duplicate import in vitest.config.ts
- 9 tests, 100% coverage on createQuery.ts, 88.6% overall

Closes COMPT-40

* chore: sync package-lock.json with @tanstack/react-query addition

* chore: switch from pnpm to npm, sync lockfile

* chore: fix prettier formatting across all files

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

- 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

* feat(COMPT-42): implement createMutation and typed cache helpers (#5)

* feat(COMPT-42): implement createMutation and typed cache helpers

- createMutation(fn) returns MutationDefinition with mutationFn and useMutation shorthand
- useMutation exposes mutate/mutateAsync/isPending/isError/error/data/reset
- invalidateQueries(client, queryDef, params?) uses queryDef key — no raw strings
- setQueryData typed updater — wrong shape is TypeScript compile error
- All exported from src/index.ts via src/query/index.ts
- 18 tests (10 mutation + 8 cache), 100% coverage on both src files, 95.94% overall

Closes COMPT-42

* chore: fix prettier formatting

* fix: suppress eslint no-unused-vars on intentionally unused mutation param

* feat(COMPT-43): add integration test suite in src/__tests__/ (#6)

- createQuery.test.tsx: queryKey shape, queryFn call, useQuery loading/success/error/enabled/rerender
- usePaginatedQuery.test.tsx: offset page navigation, data shape, cursor fetchNextPage/hasNextPage/nextCursor
- createMutation.test.tsx: idle state, mutate, isPending, data, isError, reset, mutateAsync
- cacheHelpers.test.tsx: invalidateQueries marks stale + refetch, setQueryData direct/updater/hook reflect

All 84 tests pass, 97.35% stmt coverage (target: 85%)

* feat(COMPT-43): consolidate all tests into src/__tests__/ (#7)

- Moved all co-located tests (src/query/*.test.tsx, src/index.test.ts) into src/__tests__/
- Merged unique tests from co-located files: definition shape, stable key, TData inference,
  mode assertions, initialPage, mutationFn direct call
- Deleted: src/query/createQuery.test.tsx, cacheHelpers.test.tsx, createMutation.test.tsx,
  usePaginatedQuery.test.tsx, src/index.test.ts
- 51 tests, all passing, no test files outside src/__tests__/

* feat(COMPT-44): README guide + changeset for v0.1.0 (#8)

- Rewrote README as an end-to-end usage guide for @ciscode/query-kit
- createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access
- usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage)
- createMutation + invalidateQueries full lifecycle example
- setQueryData typed updater example
- API reference table covering all exports
- Peer dep @tanstack/react-query >=5 clearly stated
- Changeset: minor bump to v0.1.0 (initial public release)

* Feat/compt 44 readme changeset (#10)

* feat(COMPT-44): README guide + changeset for v0.1.0

- Rewrote README as an end-to-end usage guide for @ciscode/query-kit
- createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access
- usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage)
- createMutation + invalidateQueries full lifecycle example
- setQueryData typed updater example
- API reference table covering all exports
- Peer dep @tanstack/react-query >=5 clearly stated
- Changeset: minor bump to v0.1.0 (initial public release)

* fix(ci): correct sonar.tests path from 'test' to 'src/__tests__'

Tests live in src/__tests__/, not test/. Also add sonar.exclusions and
sonar.test.inclusions so source files and test files are correctly separated
in SonarCloud analysis.

---------

Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com>
Zaiidmo added a commit that referenced this pull request Apr 10, 2026
* ops: updated sonar variable

* Feat/compt 40 create query factory (#3)

* feat(COMPT-40): implement createQuery factory

- Add createQuery(keyFn, fetcher) returning QueryDefinition<TParams, TData>
- TData and TParams fully inferred from fetcher signature, zero manual annotation
- queryKey returns stable readonly tuple via keyFn
- useQuery shorthand hook wraps useTanstackQuery with typed params
- Export from src/index.ts
- Add @tanstack/react-query as peerDependency (>=5) and devDependency
- Add pnpm cssstyle override to fix Node.js v22 + jsdom@28 ESM compat issue
- Fix duplicate import in vitest.config.ts
- 9 tests, 100% coverage on createQuery.ts, 88.6% overall

Closes COMPT-40

* chore: sync package-lock.json with @tanstack/react-query addition

* chore: switch from pnpm to npm, sync lockfile

* chore: fix prettier formatting across all files

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

- 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

* feat(COMPT-42): implement createMutation and typed cache helpers (#5)

* feat(COMPT-42): implement createMutation and typed cache helpers

- createMutation(fn) returns MutationDefinition with mutationFn and useMutation shorthand
- useMutation exposes mutate/mutateAsync/isPending/isError/error/data/reset
- invalidateQueries(client, queryDef, params?) uses queryDef key — no raw strings
- setQueryData typed updater — wrong shape is TypeScript compile error
- All exported from src/index.ts via src/query/index.ts
- 18 tests (10 mutation + 8 cache), 100% coverage on both src files, 95.94% overall

Closes COMPT-42

* chore: fix prettier formatting

* fix: suppress eslint no-unused-vars on intentionally unused mutation param

* feat(COMPT-43): add integration test suite in src/__tests__/ (#6)

- createQuery.test.tsx: queryKey shape, queryFn call, useQuery loading/success/error/enabled/rerender
- usePaginatedQuery.test.tsx: offset page navigation, data shape, cursor fetchNextPage/hasNextPage/nextCursor
- createMutation.test.tsx: idle state, mutate, isPending, data, isError, reset, mutateAsync
- cacheHelpers.test.tsx: invalidateQueries marks stale + refetch, setQueryData direct/updater/hook reflect

All 84 tests pass, 97.35% stmt coverage (target: 85%)

* feat(COMPT-43): consolidate all tests into src/__tests__/ (#7)

- Moved all co-located tests (src/query/*.test.tsx, src/index.test.ts) into src/__tests__/
- Merged unique tests from co-located files: definition shape, stable key, TData inference,
  mode assertions, initialPage, mutationFn direct call
- Deleted: src/query/createQuery.test.tsx, cacheHelpers.test.tsx, createMutation.test.tsx,
  usePaginatedQuery.test.tsx, src/index.test.ts
- 51 tests, all passing, no test files outside src/__tests__/

* feat(COMPT-44): README guide + changeset for v0.1.0 (#8)

- Rewrote README as an end-to-end usage guide for @ciscode/query-kit
- createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access
- usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage)
- createMutation + invalidateQueries full lifecycle example
- setQueryData typed updater example
- API reference table covering all exports
- Peer dep @tanstack/react-query >=5 clearly stated
- Changeset: minor bump to v0.1.0 (initial public release)

* Feat/compt 44 readme changeset (#10)

* feat(COMPT-44): README guide + changeset for v0.1.0

- Rewrote README as an end-to-end usage guide for @ciscode/query-kit
- createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access
- usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage)
- createMutation + invalidateQueries full lifecycle example
- setQueryData typed updater example
- API reference table covering all exports
- Peer dep @tanstack/react-query >=5 clearly stated
- Changeset: minor bump to v0.1.0 (initial public release)

* fix(ci): correct sonar.tests path from 'test' to 'src/__tests__'

Tests live in src/__tests__/, not test/. Also add sonar.exclusions and
sonar.test.inclusions so source files and test files are correctly separated
in SonarCloud analysis.

* Feat/compt 44 readme changeset (#12)

* feat(COMPT-44): README guide + changeset for v0.1.0

- Rewrote README as an end-to-end usage guide for @ciscode/query-kit
- createQuery: key builder, fetcher, useQuery shorthand, direct key/fn access
- usePaginatedQuery: offset mode (nextPage/prevPage) and cursor mode (fetchNextPage/hasNextPage)
- createMutation + invalidateQueries full lifecycle example
- setQueryData typed updater example
- API reference table covering all exports
- Peer dep @tanstack/react-query >=5 clearly stated
- Changeset: minor bump to v0.1.0 (initial public release)

* fix(ci): correct sonar.tests path from 'test' to 'src/__tests__'

Tests live in src/__tests__/, not test/. Also add sonar.exclusions and
sonar.test.inclusions so source files and test files are correctly separated
in SonarCloud analysis.

* 0.0.1

---------

Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com>
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