Skip to content

Commit 54f11e8

Browse files
committed
fix(table-core): avoid no-op page index reset updates
1 parent 961258c commit 54f11e8

4 files changed

Lines changed: 196 additions & 9 deletions

File tree

.changeset/quiet-page-reset.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@tanstack/react-table': patch
3+
'@tanstack/table-core': patch
4+
---
5+
6+
Avoid emitting a pagination page-index reset when the table is already at the reset page.

packages/table-core/src/features/RowPagination.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ export const RowPagination: TableFeature = {
215215
let registered = false
216216
let queued = false
217217

218+
const getSafePageIndex = (pageIndex: number) => {
219+
const maxPageIndex =
220+
typeof table.options.pageCount === 'undefined' ||
221+
table.options.pageCount === -1
222+
? Number.MAX_SAFE_INTEGER
223+
: table.options.pageCount - 1
224+
225+
return Math.max(0, Math.min(pageIndex, maxPageIndex))
226+
}
227+
218228
table._autoResetPageIndex = () => {
219229
if (!registered) {
220230
table._queue(() => {
@@ -254,15 +264,13 @@ export const RowPagination: TableFeature = {
254264
}
255265
table.setPageIndex = (updater) => {
256266
table.setPagination((old) => {
257-
let pageIndex = functionalUpdate(updater, old.pageIndex)
258-
259-
const maxPageIndex =
260-
typeof table.options.pageCount === 'undefined' ||
261-
table.options.pageCount === -1
262-
? Number.MAX_SAFE_INTEGER
263-
: table.options.pageCount - 1
267+
const pageIndex = getSafePageIndex(
268+
functionalUpdate(updater, old.pageIndex),
269+
)
264270

265-
pageIndex = Math.max(0, Math.min(pageIndex, maxPageIndex))
271+
if (old.pageIndex === pageIndex) {
272+
return old
273+
}
266274

267275
return {
268276
...old,

packages/table-core/src/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,15 @@ export function makeStateUpdater<K extends keyof TableState>(
9494
) {
9595
return (updater: Updater<TableState[K]>) => {
9696
;(instance as any).setState(<TTableState>(old: TTableState) => {
97+
const newValue = functionalUpdate(updater, (old as any)[key])
98+
99+
if (Object.is((old as any)[key], newValue)) {
100+
return old
101+
}
102+
97103
return {
98104
...old,
99-
[key]: functionalUpdate(updater, (old as any)[key]),
105+
[key]: newValue,
100106
}
101107
})
102108
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import {
3+
ColumnDef,
4+
createTable,
5+
functionalUpdate,
6+
getCoreRowModel,
7+
PaginationState,
8+
} from '../src'
9+
10+
type Person = {
11+
name: string
12+
}
13+
14+
const data: Person[] = [{ name: 'Ada' }]
15+
const columns: ColumnDef<Person>[] = [{ accessorKey: 'name' }]
16+
17+
function createPaginationTable(
18+
pagination: PaginationState,
19+
onPaginationChange = vi.fn(),
20+
) {
21+
const table = createTable<Person>({
22+
data,
23+
columns,
24+
getCoreRowModel: getCoreRowModel(),
25+
onPaginationChange,
26+
onStateChange() {},
27+
renderFallbackValue: '',
28+
state: {
29+
pagination,
30+
},
31+
})
32+
33+
return { table, onPaginationChange }
34+
}
35+
36+
function createStatefulPaginationTable(pagination: PaginationState) {
37+
const state = { pagination }
38+
const onStateChange = vi.fn()
39+
const table = createTable<Person>({
40+
data,
41+
columns,
42+
getCoreRowModel: getCoreRowModel(),
43+
onStateChange,
44+
renderFallbackValue: '',
45+
state,
46+
})
47+
48+
return { table, onStateChange, state }
49+
}
50+
51+
describe('RowPagination', () => {
52+
it('keeps pagination state identity when resetting to the current page index', () => {
53+
const { table, onPaginationChange } = createPaginationTable({
54+
pageIndex: 0,
55+
pageSize: 10,
56+
})
57+
const pagination = table.getState().pagination
58+
59+
table.resetPageIndex()
60+
61+
expect(onPaginationChange).toHaveBeenCalledOnce()
62+
expect(
63+
functionalUpdate(onPaginationChange.mock.calls[0][0], pagination),
64+
).toBe(pagination)
65+
})
66+
67+
it('keeps table state identity when resetting to the current page index through the default state updater', () => {
68+
const { table, onStateChange, state } = createStatefulPaginationTable({
69+
pageIndex: 0,
70+
pageSize: 10,
71+
})
72+
73+
table.resetPageIndex()
74+
75+
expect(onStateChange).toHaveBeenCalledOnce()
76+
expect(functionalUpdate(onStateChange.mock.calls[0][0], state)).toBe(state)
77+
})
78+
79+
it('does not drop a reset queued after another page index update', () => {
80+
const { table, onStateChange, state } = createStatefulPaginationTable({
81+
pageIndex: 0,
82+
pageSize: 10,
83+
})
84+
85+
table.setPageIndex(1)
86+
table.resetPageIndex()
87+
88+
expect(onStateChange).toHaveBeenCalledTimes(2)
89+
90+
const stateAfterPageChange = functionalUpdate(
91+
onStateChange.mock.calls[0][0],
92+
state,
93+
)
94+
expect(stateAfterPageChange).not.toBe(state)
95+
expect(stateAfterPageChange.pagination.pageIndex).toBe(1)
96+
97+
const stateAfterReset = functionalUpdate(
98+
onStateChange.mock.calls[1][0],
99+
stateAfterPageChange,
100+
)
101+
expect(stateAfterReset).not.toBe(stateAfterPageChange)
102+
expect(stateAfterReset.pagination.pageIndex).toBe(0)
103+
})
104+
105+
it('emits a pagination change when resetting from a different page index', () => {
106+
const { table, onPaginationChange } = createPaginationTable({
107+
pageIndex: 1,
108+
pageSize: 10,
109+
})
110+
111+
table.resetPageIndex()
112+
113+
expect(onPaginationChange).toHaveBeenCalledOnce()
114+
expect(
115+
functionalUpdate(onPaginationChange.mock.calls[0][0], {
116+
pageIndex: 1,
117+
pageSize: 10,
118+
}),
119+
).toEqual({
120+
pageIndex: 0,
121+
pageSize: 10,
122+
})
123+
})
124+
125+
it('keeps pagination state identity when auto-resetting an already reset page index', async () => {
126+
const { table, onPaginationChange } = createPaginationTable({
127+
pageIndex: 0,
128+
pageSize: 10,
129+
})
130+
const pagination = table.getState().pagination
131+
132+
table._autoResetPageIndex()
133+
await Promise.resolve()
134+
135+
table._autoResetPageIndex()
136+
await Promise.resolve()
137+
138+
expect(onPaginationChange).toHaveBeenCalledOnce()
139+
expect(
140+
functionalUpdate(onPaginationChange.mock.calls[0][0], pagination),
141+
).toBe(pagination)
142+
})
143+
144+
it('keeps pagination state identity when core row data changes and the page index is already reset', async () => {
145+
const { table, onPaginationChange } = createPaginationTable({
146+
pageIndex: 0,
147+
pageSize: 10,
148+
})
149+
const pagination = table.getState().pagination
150+
151+
table.getRowModel()
152+
await Promise.resolve()
153+
onPaginationChange.mockClear()
154+
155+
table.setOptions((old) => ({
156+
...old,
157+
data: [{ name: 'Grace' }],
158+
}))
159+
table.getRowModel()
160+
await Promise.resolve()
161+
162+
expect(onPaginationChange).toHaveBeenCalledOnce()
163+
expect(
164+
functionalUpdate(onPaginationChange.mock.calls[0][0], pagination),
165+
).toBe(pagination)
166+
})
167+
})

0 commit comments

Comments
 (0)