Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useMemo, useState } from 'react'
import { describe, it, expect } from 'vitest'
import { render, screen, waitFor } from '../../test-utils'
import DataTable from '../../../lib/components/DataTable/DataTable'
import {
ColumnDefinition,
ColumnType,
SearchConfig,
} from '../../../lib/components/DataTable/types'

type Row = { id: number; name: string }

const columns: ColumnDefinition<Record<string, unknown>>[] = [
{ field: 'name', header: 'Name', type: ColumnType.TEXT, isSortable: false },
]

const DataTableServerSearchHarness = () => {
// Same length and same first/last ids, but the middle id/name differs.
const initialData = useMemo<Row[]>(
() => [
{ id: 1, name: 'Alpha' },
{ id: 2, name: 'Beta' },
{ id: 3, name: 'Gamma' },
],
[]
)
const searchedData = useMemo<Row[]>(
() => [
{ id: 1, name: 'Alpha' },
{ id: 99, name: 'Zeta' },
{ id: 3, name: 'Gamma' },
],
[]
)

const [data, setData] = useState<Row[]>(initialData)

return (
<DataTable
title="Users"
enableSearch
serverSideSearch
idField="id"
columns={columns as unknown as ColumnDefinition<Row>[]}
data={data}
onSearchChange={(cfg: SearchConfig) => {
setData(cfg.query.trim() ? searchedData : initialData)
}}
/>
)
}

describe('DataTable (server-side search)', () => {
it('updates rendered rows when same-length results swap middle IDs', async () => {
const { user } = render(<DataTableServerSearchHarness />)

expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Beta')).toBeInTheDocument()
expect(screen.getByText('Gamma')).toBeInTheDocument()

const searchInput = screen.getByPlaceholderText('Search...')
await user.type(searchInput, 'z')

await waitFor(() => {
expect(screen.getByText('Zeta')).toBeInTheDocument()
})
expect(screen.queryByText('Beta')).not.toBeInTheDocument()
})
})
35 changes: 32 additions & 3 deletions packages/blend/lib/components/DataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -650,12 +650,40 @@ const DataTable = forwardRef(
pagination?.pageSize,
])

// Stable row ID list for the current page. Used for selection state and
// as a cheap signal to remount the tbody when results change (e.g. server-side search).
const currentPageRowIds = useMemo(() => {
return currentData.map((row) => String(row[idField]))
}, [currentData, idField])

// Monotonically increasing "dataVersion" for the current page's row IDs.
// This avoids expensive per-character hashing in TableBody while still
// forcing a remount when IDs change but length/first/last stay the same.
const [tbodyDataVersion, setTbodyDataVersion] = useState(0)
const prevPageRowIdsRef = useRef<string[] | null>(null)
useEffect(() => {
const prev = prevPageRowIdsRef.current
let changed =
prev == null || prev.length !== currentPageRowIds.length

if (!changed && prev) {
for (let i = 0; i < prev.length; i++) {
if (prev[i] !== currentPageRowIds[i]) {
changed = true
break
}
}
}

if (changed) {
setTbodyDataVersion((v) => v + 1)
}
prevPageRowIdsRef.current = currentPageRowIds
}, [currentPageRowIds])

const updateSelectAllState = (
selectedRowsState: Record<string, boolean>
) => {
const currentPageRowIds = currentData.map((row) =>
String(row[idField])
)
const selectedCurrentPageRows = currentPageRowIds.filter(
(rowId) => selectedRowsState[rowId]
)
Expand Down Expand Up @@ -1634,6 +1662,7 @@ const DataTable = forwardRef(
{currentData.length > 0 && (
<TableBodyComponent
currentData={currentData}
dataVersion={tbodyDataVersion}
visibleColumns={
effectiveVisibleColumns as ColumnDefinition<
Record<string, unknown>
Expand Down
15 changes: 11 additions & 4 deletions packages/blend/lib/components/DataTable/TableBody/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ const TableBody = forwardRef<
(
{
currentData,
dataVersion,
visibleColumns,
idField,
tableTitle,
Expand Down Expand Up @@ -637,10 +638,16 @@ const TableBody = forwardRef<
skeletonVariant: variant,
}
}
const tbodyKey =
currentData.length > 0
? `tbody-${currentData.length}-${String(currentData[0][idField])}-${String(currentData[currentData.length - 1][idField])}`
: 'tbody-empty'
const tbodyKey = useMemo(() => {
if (dataVersion !== undefined) return `tbody-${String(dataVersion)}`

const len = currentData.length
if (len === 0) return 'tbody-empty'

const firstId = String(currentData[0][idField])
const lastId = String(currentData[len - 1][idField])
return `tbody-${len}-${firstId}-${lastId}`
}, [currentData, dataVersion, idField])

return (
<motion.tbody
Expand Down
1 change: 1 addition & 0 deletions packages/blend/lib/components/DataTable/TableBody/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SkeletonVariant } from '../../Skeleton/skeleton.tokens'

export type TableBodyProps<T extends Record<string, unknown>> = {
currentData: T[]
dataVersion?: number | string
visibleColumns: ColumnDefinition<T>[]
idField: string
tableTitle?: string
Expand Down
Loading