diff --git a/packages/blend/__tests__/components/DataTable/DataTable.serverSideSearch.remount.test.tsx b/packages/blend/__tests__/components/DataTable/DataTable.serverSideSearch.remount.test.tsx new file mode 100644 index 000000000..f84e93ad0 --- /dev/null +++ b/packages/blend/__tests__/components/DataTable/DataTable.serverSideSearch.remount.test.tsx @@ -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>[] = [ + { 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( + () => [ + { id: 1, name: 'Alpha' }, + { id: 2, name: 'Beta' }, + { id: 3, name: 'Gamma' }, + ], + [] + ) + const searchedData = useMemo( + () => [ + { id: 1, name: 'Alpha' }, + { id: 99, name: 'Zeta' }, + { id: 3, name: 'Gamma' }, + ], + [] + ) + + const [data, setData] = useState(initialData) + + return ( + []} + 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() + + 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() + }) +}) diff --git a/packages/blend/lib/components/DataTable/DataTable.tsx b/packages/blend/lib/components/DataTable/DataTable.tsx index cd74b0213..5f2df2cb7 100644 --- a/packages/blend/lib/components/DataTable/DataTable.tsx +++ b/packages/blend/lib/components/DataTable/DataTable.tsx @@ -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(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 ) => { - const currentPageRowIds = currentData.map((row) => - String(row[idField]) - ) const selectedCurrentPageRows = currentPageRowIds.filter( (rowId) => selectedRowsState[rowId] ) @@ -1634,6 +1662,7 @@ const DataTable = forwardRef( {currentData.length > 0 && ( diff --git a/packages/blend/lib/components/DataTable/TableBody/index.tsx b/packages/blend/lib/components/DataTable/TableBody/index.tsx index 24bcee66a..be1e2fc9e 100644 --- a/packages/blend/lib/components/DataTable/TableBody/index.tsx +++ b/packages/blend/lib/components/DataTable/TableBody/index.tsx @@ -535,6 +535,7 @@ const TableBody = forwardRef< ( { currentData, + dataVersion, visibleColumns, idField, tableTitle, @@ -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 ( > = { currentData: T[] + dataVersion?: number | string visibleColumns: ColumnDefinition[] idField: string tableTitle?: string