Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/framework/react/guide/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ No pagination row model is needed for server-side pagination, but if you have pr

#### Page Count and Row Count

The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case.
The table instance will have no way of knowing how many rows/pages there are in total in your back-end unless you tell it. Provide either the `rowCount` or `pageCount` table option to let the table instance know how many pages there are in total. If you provide a `rowCount`, the table instance will calculate the `pageCount` internally from `rowCount` and `pageSize`. Otherwise, you can directly provide the `pageCount` if you already have it. If you don't know the page count, you can just pass in `-1` for the `pageCount`, but the `getCanNextPage` and `getCanPreviousPage` row model functions will always return `true` in this case. When you do know whether more pages are available (for example, a server response that says "has more"), you can override this by setting the optional `canNextPage`/`canPreviousPage` values on the [pagination state](#pagination-state) directly.

```tsx
import {
Expand Down Expand Up @@ -121,6 +121,10 @@ The `pagination` state is an object that contains the following properties:

- `pageIndex`: The current page index (zero-based).
- `pageSize`: The current page size.
- `canNextPage` _(optional)_: Explicitly overrides the value returned by `getCanNextPage()`. Useful for manual/server-side pagination (e.g. news feeds) where the total `pageCount`/`rowCount` is unknown.
- `canPreviousPage` _(optional)_: Explicitly overrides the value returned by `getCanPreviousPage()`. Useful for manual/server-side pagination where the total `pageCount`/`rowCount` is unknown.

> **Note**: The pagination navigation APIs (`setPageIndex`, `setPageSize`, `nextPage`, etc.) preserve `canNextPage`/`canPreviousPage` when they update the state. However, if you replace the whole `pagination` object yourself (for example inside `onPaginationChange`), you must re-supply these flags to keep the overrides in effect.

For reactive reads that should re-render your UI, use `table.state.pagination`. In event handlers, you can read the current snapshot with `table.atoms.pagination.get()`, but this read does not subscribe the component to future changes.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import type { TableFeatures } from '../../types/TableFeatures'
export interface PaginationState {
pageIndex: number
pageSize: number
/**
* When manually controlling pagination, supply `canNextPage` explicitly to override the derived value. This is useful when working with news feeds or other server paginated data where `pageCount` or `rowCount` is unknown.
*/
canNextPage?: boolean
/**
* When manually controlling pagination, supply `canPreviousPage` explicitly to override the derived value. This is useful when working with news feeds or other server paginated data where `pageCount` or `rowCount` is unknown.
*/
canPreviousPage?: boolean
}

export interface TableState_RowPagination {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,14 @@ export function table_getCanPreviousPage<
TFeatures extends TableFeatures,
TData extends RowData,
>(table: Table_Internal<TFeatures, TData>) {
return (table.atoms.pagination?.get()?.pageIndex ?? 0) > 0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Previously this used ?? 0. However this is now updated to use the same defaultPageIndex const as table_getCanNextPage for consistency

const pagination = table.atoms.pagination?.get()
const { canPreviousPage, pageIndex } = pagination ?? {}
Comment on lines +254 to +255

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Destructuring before any return condition ensures reactivity for lazy eval frameworks are maintained.

This is important in the case where pageIndex is declared as a lazy getter in frameworks such as Solid/Angular/Ember.


if (canPreviousPage !== undefined) {
return canPreviousPage
}

return (pageIndex ?? defaultPageIndex) > 0
}

/**
Expand All @@ -269,10 +276,16 @@ export function table_getCanNextPage<
TFeatures extends TableFeatures,
TData extends RowData,
>(table: Table_Internal<TFeatures, TData>) {
const pageIndex = table.atoms.pagination?.get()?.pageIndex ?? defaultPageIndex

const pagination = table.atoms.pagination?.get()
const pageIndex = pagination?.pageIndex ?? defaultPageIndex
// Read `pageCount` before any early return so the reactivity system always
// subscribes to its dependencies (pageSize atom + pre-paginated row model).
const pageCount = table_getPageCount(table)

if (pagination?.canNextPage !== undefined) {
return pagination.canNextPage
}

if (pageCount === -1) {
return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, expect, it } from 'vitest'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Net new unit tests.

import {
constructTable,
coreFeatures,
createPaginatedRowModel,
rowPaginationFeature,
tableFeatures,
} from '../../../../src'
import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings'
import { generateTestColumnDefs } from '../../../fixtures/data/generateTestColumnDefs'
import { generateTestData } from '../../../fixtures/data/generateTestData'
import type { PaginationState } from '../../../../src'
import type { Person } from '../../../fixtures/data/types'

const features = tableFeatures({
...coreFeatures,
rowPaginationFeature,
paginatedRowModel: createPaginatedRowModel(),
})

function createPaginationTable(options?: {
rowLength?: number
pagination?: Partial<PaginationState>
pageCount?: number
rowCount?: number
manualPagination?: boolean
}) {
const data = generateTestData(options?.rowLength ?? 30)
const columns = generateTestColumnDefs<typeof features>(data)

return constructTable<typeof features, Person>({
data,
columns,
getSubRows: (row) => row.subRows,
manualPagination: options?.manualPagination,
pageCount: options?.pageCount,
rowCount: options?.rowCount,
initialState: {
pagination: {
pageIndex: 0,
pageSize: 10,
...options?.pagination,
},
},
features: {
...features,
coreReactivityFeature: storeReactivityBindings(),
},
})
}

describe('table_getCanPreviousPage', () => {
it('returns false on the first page (pageIndex 0)', () => {
const table = createPaginationTable({ pagination: { pageIndex: 0 } })
expect(table.getCanPreviousPage()).toBe(false)
})

it('returns true when past the first page', () => {
const table = createPaginationTable({ pagination: { pageIndex: 1 } })
expect(table.getCanPreviousPage()).toBe(true)
})

it('honors an explicit canPreviousPage override on the first page', () => {
const table = createPaginationTable({
pagination: { pageIndex: 0, canPreviousPage: true },
})
expect(table.getCanPreviousPage()).toBe(true)
})

it('honors an explicit canPreviousPage=false override past the first page', () => {
const table = createPaginationTable({
pagination: { pageIndex: 1, canPreviousPage: false },
})
expect(table.getCanPreviousPage()).toBe(false)
})
})

describe('table_getCanNextPage', () => {
it('returns true when there are more pages', () => {
// 30 rows / pageSize 10 => 3 pages, currently on page 0
const table = createPaginationTable({ pagination: { pageIndex: 0 } })
expect(table.getCanNextPage()).toBe(true)
})

it('returns false on the last page', () => {
const table = createPaginationTable({ pagination: { pageIndex: 2 } })
expect(table.getCanNextPage()).toBe(false)
})

it('returns true for an unknown page count (pageCount -1)', () => {
const table = createPaginationTable({
manualPagination: true,
pageCount: -1,
})
expect(table.getCanNextPage()).toBe(true)
})

it('returns false for an empty page count (pageCount 0)', () => {
const table = createPaginationTable({
manualPagination: true,
pageCount: 0,
})
expect(table.getCanNextPage()).toBe(false)
})

it('honors canNextPage=false override even when pageCount is unknown (-1)', () => {
const table = createPaginationTable({
manualPagination: true,
pageCount: -1,
pagination: { canNextPage: false },
})
expect(table.getCanNextPage()).toBe(false)
})

it('honors canNextPage=true override even when pageCount is 0', () => {
const table = createPaginationTable({
manualPagination: true,
pageCount: 0,
pagination: { canNextPage: true },
})
expect(table.getCanNextPage()).toBe(true)
})
})