diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee7e1914a..77c2779a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: - 'source/src/**' - 'examples/src/**/*.spec.ts' - 'examples/src/**/*.page.tsx' + - 'examples/src/**/*.ts' concurrency: group: ${{ github.workflow }} diff --git a/examples/perf-baselines.ci.json b/examples/perf-baselines.ci.json index 67b26ebc5..d96f36130 100644 --- a/examples/perf-baselines.ci.json +++ b/examples/perf-baselines.ci.json @@ -1,23 +1,107 @@ { - "tests/table/perf/default:horizontal-scrolling": { - "scriptingTime": 111.42, - "renderingTime": 26.61, - "paintingTime": 8.89, - "totalTime": 714.34, + "tests/table/perf/default:horizontal-scrolling with tracing": { + "scriptingTime": 135.73, + "renderingTime": 23.92, + "paintingTime": 8.11, + "totalTime": 628.45, + "threshold": 10 + }, + "tests/table/perf/flashing/default:perf should be fine when single click": { + "scriptingTime": 22.01, + "renderingTime": 0.48, + "paintingTime": 0.07, + "totalTime": 155.95, + "threshold": 10 + }, + "tests/table/perf/flashing/default:should be able to collapse and expand nodes with no error": { + "scriptingTime": 163.94, + "renderingTime": 81.91, + "paintingTime": 5.63, + "totalTime": 451.16, + "threshold": 10 + }, + "tests/table/perf/flashing/default:perf should remain stable for 50 clicks": { + "scriptingTime": 847.02, + "renderingTime": 35.58, + "paintingTime": 28.71, + "totalTime": 3126.17, + "threshold": 10 + }, + "tests/table/props/column/column-change:works correctly": { + "scriptingTime": 39.5, + "renderingTime": 4.08, + "paintingTime": 0.88, + "totalTime": 251.63, + "threshold": 10 + }, + "tests/table/props/column-pinning/virtualization:should work correctly": { + "scriptingTime": 162.82, + "renderingTime": 8.74, + "paintingTime": 0.9, + "totalTime": 230.74, + "threshold": 10 + }, + "tests/table/props/data/basic-add-data:insert data": { + "scriptingTime": 198.43, + "renderingTime": 4.35, + "paintingTime": 1.37, + "totalTime": 301.11, + "threshold": 10 + }, + "tests/table/props/data/basic-update:via API should not rerender header": { + "scriptingTime": 23.61, + "renderingTime": 2.46, + "paintingTime": 0.97, + "totalTime": 221.55, + "threshold": 10 + }, + "tests/table/props/data/editing/column-editor:should use custom editor when configured": { + "scriptingTime": 118.99, + "renderingTime": 32.66, + "paintingTime": 5.04, + "totalTime": 628.58, + "threshold": 10 + }, + "tests/table/props/data/editing/persistEdit:should not persist changes to the id column": { + "scriptingTime": 56.02, + "renderingTime": 11.96, + "paintingTime": 1.26, + "totalTime": 294.24, + "threshold": 10 + }, + "tests/table/props/data/editing/persistEdit:should call and wait for async persistEdit correctly": { + "scriptingTime": 81.64, + "renderingTime": 20.67, + "paintingTime": 2.25, + "totalTime": 575.84, + "threshold": 10 + }, + "tests/table/props/data/edit-with-delay:on string column": { + "scriptingTime": 322.51, + "renderingTime": 24.6, + "paintingTime": 17.67, + "totalTime": 4084.08, + "threshold": 10 + }, + "tests/table/props/filtering/filter-value/client-side:Filters correctly": { + "scriptingTime": 254.35, + "renderingTime": 29.37, + "paintingTime": 7.81, + "totalTime": 455.72, "threshold": 10 }, "tests/table/props/data/scrolling:scrolls the table with performance tracing": { - "scriptingTime": 304.86, - "renderingTime": 94.59, - "paintingTime": 49.87, - "totalTime": 652.31, + "scriptingTime": 473.23, + "renderingTime": 141, + "paintingTime": 57.1, + "totalTime": 906.55, "threshold": 10 }, "tests/table/props/data/update:clicks update button 30 times with performance tracing": { - "scriptingTime": 579.33, - "renderingTime": 36.93, - "paintingTime": 51.26, - "totalTime": 3089.28, + "scriptingTime": 642.03, + "renderingTime": 43.1, + "paintingTime": 45.53, + "totalTime": 3284.21, "threshold": 10 } } diff --git a/examples/perf-baselines.radubrehar.json b/examples/perf-baselines.radubrehar.json index 99729dccd..a89149a58 100644 --- a/examples/perf-baselines.radubrehar.json +++ b/examples/perf-baselines.radubrehar.json @@ -1,11 +1,4 @@ { - "tests/table/props/data/update:clicks update button 30 times with performance tracing": { - "scriptingTime": 1464.83, - "renderingTime": 23.22, - "paintingTime": 13.42, - "totalTime": 3736.29, - "threshold": 10 - }, "tests/table/props/data/scrolling:clicks update button 30 times with performance tracing": { "scriptingTime": 1919.15, "renderingTime": 24.01, @@ -14,10 +7,115 @@ "threshold": 10 }, "tests/table/props/data/scrolling:scrolls the table with performance tracing": { - "scriptingTime": 546.06, - "renderingTime": 33.65, - "paintingTime": 17.5, - "totalTime": 679.69, + "scriptingTime": 256.07, + "renderingTime": 33.59, + "paintingTime": 7.53, + "totalTime": 407.22, + "threshold": 10 + }, + "tests/table/perf/default:horizontal-scrolling": { + "scriptingTime": 133.1, + "renderingTime": 11.79, + "paintingTime": 3.78, + "totalTime": 638.69, + "threshold": 10 + }, + "tests/table/props/data/basic-update:via API should not rerender header": { + "scriptingTime": 64.32, + "renderingTime": 1.21, + "paintingTime": 0.2, + "totalTime": 114.46, + "threshold": 10 + }, + "tests/table/props/column/column-change:works correctly": { + "scriptingTime": 81.79, + "renderingTime": 2.38, + "paintingTime": 0.52, + "totalTime": 195.19, + "threshold": 10 + }, + "tests/table/props/data/editing/column-editor:should use custom editor when configured": { + "scriptingTime": 126.92, + "renderingTime": 11.75, + "paintingTime": 1.58, + "totalTime": 515.07, + "threshold": 10 + }, + "tests/table/props/data/editing/persistEdit:should call and wait for async persistEdit correctly": { + "scriptingTime": 124.13, + "renderingTime": 7.72, + "paintingTime": 0.84, + "totalTime": 433.35, + "threshold": 10 + }, + "tests/table/props/data/editing/persistEdit:should not persist changes to the id column": { + "scriptingTime": 111.24, + "renderingTime": 6.03, + "paintingTime": 0.96, + "totalTime": 221.65, + "threshold": 10 + }, + "tests/table/props/data/basic-add-data:insert data": { + "scriptingTime": 85.44, + "renderingTime": 1.87, + "paintingTime": 0.71, + "totalTime": 136.82, + "threshold": 10 + }, + "tests/table/props/data/edit-with-delay:on string column": { + "scriptingTime": 117.09, + "renderingTime": 9.03, + "paintingTime": 1.78, + "totalTime": 3834.25, + "threshold": 10 + }, + "tests/table/props/filtering/filter-value/client-side:Filters correctly": { + "scriptingTime": 109.36, + "renderingTime": 8.93, + "paintingTime": 2.03, + "totalTime": 265.19, + "threshold": 10 + }, + "tests/table/perf/flashing/default:perf should be fine when single click": { + "scriptingTime": 71.41, + "renderingTime": 0.39, + "paintingTime": 0.06, + "totalTime": 89.04, + "threshold": 10 + }, + "tests/table/perf/flashing/default:should be able to collapse and expand nodes with no error": { + "scriptingTime": 152.55, + "renderingTime": 25.5, + "paintingTime": 2.65, + "totalTime": 257.12, + "threshold": 10 + }, + "tests/table/perf/flashing/default:perf should remain stable for 50 clicks": { + "scriptingTime": 638.96, + "renderingTime": 23.08, + "paintingTime": 11, + "totalTime": 2647.44, + "threshold": 10 + }, + "tests/table/props/data/update:clicks update button 30 times with performance tracing": { + "scriptingTime": 382.65, + "renderingTime": 22.89, + "paintingTime": 12.41, + "totalTime": 2626.64, + "threshold": 10 + }, + "tests/table/perf/default:horizontal-scrolling.": { + "scriptingTime": 293.15, + "renderingTime": 10.42, + "paintingTime": 2.43, + "totalTime": 734.17, + "threshold": 10 + }, + "tests/table/perf/default:horizontal-scrolling with tracing": { + "scriptingTime": 121.15, + "renderingTime": 11.14, + "paintingTime": 3, + "totalTime": 645.6, "threshold": 10 } } diff --git a/examples/src/pages/components/datasource/index.page.tsx b/examples/src/pages/components/datasource/index.page.tsx index 1536043b2..f560f8621 100644 --- a/examples/src/pages/components/datasource/index.page.tsx +++ b/examples/src/pages/components/datasource/index.page.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; -import { DataSource, useDataSourceState } from '@infinite-table/infinite-react'; +import { + DataSource, + useDataSourceSelector, +} from '@infinite-table/infinite-react'; interface Person { name: string; @@ -13,9 +16,13 @@ const persons: Person[] = [ ]; const Cmp = () => { - const ds = useDataSourceState(); + const { dataArray, loading } = useDataSourceSelector((ctx) => { + return { + dataArray: ctx.dataSourceState.dataArray, + loading: ctx.dataSourceState.loading, + }; + }); - const { dataArray, loading } = ds; return (
{loading ? 'loading' : null} diff --git a/examples/src/pages/tests/datasource/default.page.tsx b/examples/src/pages/tests/datasource/default.page.tsx index b83f7b8eb..a168cb88a 100644 --- a/examples/src/pages/tests/datasource/default.page.tsx +++ b/examples/src/pages/tests/datasource/default.page.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; -import { DataSource, useDataSourceState } from '@infinite-table/infinite-react'; +import { + DataSource, + useDataSourceSelector, +} from '@infinite-table/infinite-react'; interface Person { name: string; @@ -14,9 +17,12 @@ const persons: Person[] = [ ]; const Cmp = () => { - const ds = useDataSourceState(); - - const { dataArray, loading } = ds; + const { dataArray, loading } = useDataSourceSelector((ctx) => { + return { + dataArray: ctx.dataSourceState.dataArray, + loading: ctx.dataSourceState.loading, + }; + }); return (
diff --git a/examples/src/pages/tests/datasource/grouped-ssr.page.tsx b/examples/src/pages/tests/datasource/grouped-ssr.page.tsx index 8c3e168db..54413427d 100644 --- a/examples/src/pages/tests/datasource/grouped-ssr.page.tsx +++ b/examples/src/pages/tests/datasource/grouped-ssr.page.tsx @@ -3,13 +3,16 @@ import * as React from 'react'; import { DataSource, DataSourceData, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; const Cmp = () => { - const ds = useDataSourceState(); - - const { dataArray, loading } = ds; + const { dataArray, loading } = useDataSourceSelector((ctx) => { + return { + dataArray: ctx.dataSourceState.dataArray, + loading: ctx.dataSourceState.loading, + }; + }); return (
diff --git a/examples/src/pages/tests/datasource/sortinfo-controlled.page.tsx b/examples/src/pages/tests/datasource/sortinfo-controlled.page.tsx index 2de247c14..0331ea894 100644 --- a/examples/src/pages/tests/datasource/sortinfo-controlled.page.tsx +++ b/examples/src/pages/tests/datasource/sortinfo-controlled.page.tsx @@ -1,15 +1,18 @@ import { DataSource, DataSourceSortInfo, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { Person, persons } from './sortPersons'; const Cmp = () => { - const ds = useDataSourceState(); - - const { dataArray, loading } = ds; + const { dataArray, loading } = useDataSourceSelector((ctx) => { + return { + dataArray: ctx.dataSourceState.dataArray, + loading: ctx.dataSourceState.loading, + }; + }); return (
diff --git a/examples/src/pages/tests/table/custom-structure.page.tsx b/examples/src/pages/tests/table/custom-structure.page.tsx index 919960bae..53123287a 100644 --- a/examples/src/pages/tests/table/custom-structure.page.tsx +++ b/examples/src/pages/tests/table/custom-structure.page.tsx @@ -4,7 +4,7 @@ import { InfiniteTableColumn, DataSourceGroupBy, components, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; import * as React from 'react'; @@ -98,7 +98,11 @@ export default function App() { } function AppGrid() { - const { dataArray } = useDataSourceState(); + const { dataArray } = useDataSourceSelector((ctx) => { + return { + dataArray: ctx.dataSourceState.dataArray, + }; + }); return (
= { field: 'salary', type: 'number', render: ({ value }) => { - const { state } = useInfiniteTable(); + const { brain } = useInfiniteTableSelector((ctx) => { + return { + brain: ctx.state.brain, + }; + }); - setBrain(state.brain); + setBrain(brain); return value; }, }, diff --git a/examples/src/pages/tests/table/master-detail/default.page.tsx b/examples/src/pages/tests/table/master-detail/default.page.tsx index aeaa98ef7..58f2cd512 100644 --- a/examples/src/pages/tests/table/master-detail/default.page.tsx +++ b/examples/src/pages/tests/table/master-detail/default.page.tsx @@ -7,6 +7,7 @@ import { InfiniteTablePropColumns, InfiniteTableRowInfo, DataSourceProps, + useMasterRowInfo, } from '@infinite-table/infinite-react'; type Developer = { @@ -39,6 +40,17 @@ const columns: InfiniteTablePropColumns = { //
// ); // }, + components: { + HeaderCell: () => { + const masterRowInfo = useMasterRowInfo(); + + if (masterRowInfo) { + return
Hello {masterRowInfo.data?.firstName}
; + } + + return
Hello
; + }, + }, }, firstName: { field: 'firstName' }, diff --git a/examples/src/pages/tests/table/master-detail/use-master-row-info.page.tsx b/examples/src/pages/tests/table/master-detail/use-master-row-info.page.tsx new file mode 100644 index 000000000..56e0f3f79 --- /dev/null +++ b/examples/src/pages/tests/table/master-detail/use-master-row-info.page.tsx @@ -0,0 +1,248 @@ +import * as React from 'react'; +import styles from './default.module.css'; + +import { + InfiniteTable, + DataSource, + InfiniteTablePropColumns, + DataSourceProps, + useMasterRowInfo, + useDataSourceState, + DataSourceState, + useInfiniteHeaderCell, +} from '@infinite-table/infinite-react'; + +type Developer = { + id: number; + firstName: string; + lastName: string; + country: string; + city: string; + currency: string; + preferredLanguage: string; + stack: string; + canDesign: 'yes' | 'no'; + hobby: string; + salary: number; + age: number; + streetName: string; + streetNo: number; + streetPrefix: string; +}; + +function HeaderWithCount() { + const dataArray = useDataSourceState( + (state: DataSourceState) => state.dataArray, + ); + + const { column } = useInfiniteHeaderCell(); + const length = dataArray.length; + return ( +
+ {column.id} {length} +
+ ); +} +const columns: InfiniteTablePropColumns = { + id: { + field: 'id', + renderHeader: () => { + return ; + }, + }, + + firstName: { field: 'firstName' }, + age: { + field: 'age', + type: ['number'], + }, + salary: { + field: 'salary', + type: ['number', 'currency'], + style: { + color: 'red', + }, + renderRowDetailIcon: true, + }, + + canDesign: { field: 'canDesign' }, + preferredLanguage: { field: 'preferredLanguage' }, + city: { field: 'city' }, + lastName: { field: 'lastName' }, + hobby: { field: 'hobby' }, + stack: { field: 'stack' }, + streetName: { field: 'streetName' }, + currency: { field: 'currency' }, +}; + +const dataSource = () => { + return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') + .then((r) => r.json()) + .then((data: Developer[]) => data); +}; + +const detailDataSource: DataSourceProps['data'] = ({ + sortInfo, + filterValue, + masterRowInfo, +}) => { + if (sortInfo && !Array.isArray(sortInfo)) { + sortInfo = [sortInfo]; + } + console.log(masterRowInfo?.data, 'master'); + + const args = [ + filterValue + ? 'filterBy=' + + JSON.stringify( + filterValue.map(({ field, filter }) => { + return { + field: field, + value: filter.value, + operator: filter.operator, + }; + }), + ) + : null, + sortInfo + ? 'sortInfo=' + + JSON.stringify( + sortInfo.map((s: any) => ({ + field: s.field, + dir: s.dir, + })), + ) + : null, + ] + .filter(Boolean) + .join('&'); + + return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers10-sql?' + args) + .then((r) => r.json()) + .then((response: { data: Developer[] }) => { + const { data } = response; + + return new Promise((resolve) => { + // setTimeout(() => { + resolve( + data.map((x) => { + return { + ...x, + id: `000${x.id}` as any as number, + } as Developer; + }), + ); + // }, 1000); + }); + }); +}; + +const detailCols: InfiniteTablePropColumns = { + firstNameChildColumn: { + field: 'firstName', + + renderHeader: () => { + return ; + }, + }, + age: { field: 'age' }, + salary: { field: 'salary' }, + canDesign: { field: 'canDesign' }, + preferredLanguage: { field: 'preferredLanguage' }, + city: { field: 'city' }, + lastName: { field: 'lastName' }, + hobby: { field: 'hobby' }, + stack: { field: 'stack' }, + streetName: { field: 'streetName' }, + currency: { field: 'currency' }, +}; + +const detailsDOMProps = { + style: { + flex: 1, + }, +}; + +function RowDetail() { + const rowInfo = useMasterRowInfo(); + + if (!rowInfo) { + return null; + } + return ( +
+
+ + +
+ + debugId={`datasource-detail-${rowInfo.id}`} + data={detailDataSource} + shouldReloadData={{ + filterValue: true, + sortInfo: true, + }} + primaryKey="id" + defaultGroupBy={[ + { + field: 'stack', + }, + ]} + defaultFilterValue={[]} + key={rowInfo.id} + > + + debugId={`infinite-detail-${rowInfo.id}`} + domProps={detailsDOMProps} + columns={detailCols} + /> + +
+ ); +} + +export default function DataTestPage() { + return ( + + data={dataSource} primaryKey="id"> + + debugId="root-infinite" + columnDefaultWidth={130} + rowHeight={50} + defaultRowDetailState={{ + expandedRows: [1, 2], + collapsedRows: true, + }} + rowDetailCache={false} + components={{ + RowDetail, + }} + domProps={{ + className: styles.CustomRowHeight, + style: { + margin: '5px', + height: '80vh', + border: '1px solid gray', + position: 'relative', + }, + }} + columns={columns} + /> + + + ); +} diff --git a/examples/src/pages/tests/table/master-detail/use-master-row-info.spec.ts b/examples/src/pages/tests/table/master-detail/use-master-row-info.spec.ts new file mode 100644 index 000000000..11c991583 --- /dev/null +++ b/examples/src/pages/tests/table/master-detail/use-master-row-info.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@testing'; + +export default test.describe.parallel('Master-detail', () => { + test('should correctly render child row and useMasterRowInfo should work', async ({ + page, + tableModel, + }) => { + await page.waitForInfinite(); + + const firstChildColumn = tableModel.withColumn('firstNameChildColumn'); + expect(await firstChildColumn.getHeader()).toBe('firstNameChildColumn 13'); + + const idCol = tableModel.withColumn('id'); + expect(await idCol.getHeader()).toBe('id 100'); + + const firstNameInputs = page.locator('input[name="first name"]'); + const values = await firstNameInputs.evaluateAll((nodes) => { + return (nodes as HTMLInputElement[]).map((node) => node.value); + }); + expect(values).toEqual(['Axel', 'Gonzalo']); + }); +}); diff --git a/examples/src/pages/tests/table/perf/default.page.tsx b/examples/src/pages/tests/table/perf/default.page.tsx index 8ce2aff1a..f6e463b81 100644 --- a/examples/src/pages/tests/table/perf/default.page.tsx +++ b/examples/src/pages/tests/table/perf/default.page.tsx @@ -23,12 +23,7 @@ const getColumns = (count: number) => { field: colName, // defaultWidth: Math.max(COL_DEFAULT_WIDTH + index, 40), render: ((i: number, { rowIndex, column }: any) => { - // console.log('render', { rowIndex, columnName: column.id }); - return ( - - Row {rowIndex}, col {column.field} = {i} - - ); + return {`Row ${rowIndex}, col ${column.field} = ${i}`}; }).bind(null, i), }; columns[colName] = column; diff --git a/examples/src/pages/tests/table/perf/default.spec.ts b/examples/src/pages/tests/table/perf/default.spec.ts index d8cab1ac5..f20447d31 100644 --- a/examples/src/pages/tests/table/perf/default.spec.ts +++ b/examples/src/pages/tests/table/perf/default.spec.ts @@ -1,7 +1,11 @@ import { test } from '@testing'; export default test.describe.parallel('Tracing', () => { - test('horizontal-scrolling', async ({ page, apiModel, tracingModel }) => { + test('horizontal-scrolling with tracing', async ({ + page, + apiModel, + tracingModel, + }) => { await page.waitForInfinite(); const stop = await tracingModel.start(); diff --git a/examples/src/pages/tests/table/perf/flashing/data.ts b/examples/src/pages/tests/table/perf/flashing/data.ts new file mode 100644 index 000000000..5c5587924 --- /dev/null +++ b/examples/src/pages/tests/table/perf/flashing/data.ts @@ -0,0 +1,520 @@ +export type FileSystemNode = { + id: string; + name: string; + type: 'folder' | 'file'; + extension?: string; + mimeType?: string; + sizeInKB: number; + children?: FileSystemNode[]; +}; + +export const dataSource = () => { + const nodes: FileSystemNode[] = [ + { + id: '1', + name: 'Documents', + sizeInKB: 15400, + type: 'folder', + children: [ + { + id: '10', + name: 'Report.docx', + sizeInKB: 210, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '11', + name: 'Budget.xlsx', + sizeInKB: 340, + type: 'file', + extension: 'xlsx', + mimeType: 'application/vnd.ms-excel', + }, + { + id: '12', + name: 'Presentation.pptx', + sizeInKB: 1200, + type: 'file', + extension: 'pptx', + mimeType: 'application/vnd.ms-powerpoint', + }, + { + id: '13', + name: 'Notes.txt', + sizeInKB: 45, + type: 'file', + extension: 'txt', + mimeType: 'text/plain', + }, + { + id: '14', + name: 'Contract.pdf', + sizeInKB: 890, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '15', + name: 'Invoice.pdf', + sizeInKB: 120, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '16', + name: 'Meeting-Minutes.docx', + sizeInKB: 180, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '17', + name: 'Project-Plan.xlsx', + sizeInKB: 560, + type: 'file', + extension: 'xlsx', + mimeType: 'application/vnd.ms-excel', + }, + { + id: '18', + name: 'Resume.pdf', + sizeInKB: 230, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '19', + name: 'Cover-Letter.docx', + sizeInKB: 95, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '110', + name: 'Timesheet.xlsx', + sizeInKB: 280, + type: 'file', + extension: 'xlsx', + mimeType: 'application/vnd.ms-excel', + }, + { + id: '111', + name: 'Proposal.pdf', + sizeInKB: 670, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '112', + name: 'Agenda.docx', + sizeInKB: 110, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '113', + name: 'Financial-Report.xlsx', + sizeInKB: 920, + type: 'file', + extension: 'xlsx', + mimeType: 'application/vnd.ms-excel', + }, + { + id: '114', + name: 'Memo.txt', + sizeInKB: 32, + type: 'file', + extension: 'txt', + mimeType: 'text/plain', + }, + { + id: '115', + name: 'Policy.pdf', + sizeInKB: 540, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '116', + name: 'Guidelines.docx', + sizeInKB: 310, + type: 'file', + extension: 'docx', + mimeType: 'application/msword', + }, + { + id: '117', + name: 'Schedule.xlsx', + sizeInKB: 190, + type: 'file', + extension: 'xlsx', + mimeType: 'application/vnd.ms-excel', + }, + { + id: '118', + name: 'Summary.pdf', + sizeInKB: 420, + type: 'file', + extension: 'pdf', + mimeType: 'application/pdf', + }, + { + id: '119', + name: 'Checklist.txt', + sizeInKB: 28, + type: 'file', + extension: 'txt', + mimeType: 'text/plain', + }, + ], + }, + { + id: '2', + name: 'Desktop', + sizeInKB: 8900, + type: 'folder', + children: [ + { + id: '20', + name: 'Screenshot-1.png', + sizeInKB: 450, + type: 'file', + extension: 'png', + mimeType: 'image/png', + }, + { + id: '21', + name: 'Screenshot-2.png', + sizeInKB: 520, + type: 'file', + extension: 'png', + mimeType: 'image/png', + }, + { + id: '22', + name: 'TODO.txt', + sizeInKB: 15, + type: 'file', + extension: 'txt', + mimeType: 'text/plain', + }, + { + id: '23', + name: 'Shortcut.lnk', + sizeInKB: 2, + type: 'file', + extension: 'lnk', + mimeType: 'application/x-ms-shortcut', + }, + { + id: '24', + name: 'Wallpaper.jpg', + sizeInKB: 1200, + type: 'file', + extension: 'jpg', + mimeType: 'image/jpeg', + }, + { + id: '25', + name: 'Icon.ico', + sizeInKB: 48, + type: 'file', + extension: 'ico', + mimeType: 'image/x-icon', + }, + { + id: '26', + name: 'Backup.zip', + sizeInKB: 3400, + type: 'file', + extension: 'zip', + mimeType: 'application/zip', + }, + { + id: '27', + name: 'Config.json', + sizeInKB: 12, + type: 'file', + extension: 'json', + mimeType: 'application/json', + }, + { + id: '28', + name: 'Data.csv', + sizeInKB: 340, + type: 'file', + extension: 'csv', + mimeType: 'text/csv', + }, + { + id: '29', + name: 'Log.txt', + sizeInKB: 89, + type: 'file', + extension: 'txt', + mimeType: 'text/plain', + }, + { + id: '210', + name: 'Archive.rar', + sizeInKB: 2100, + type: 'file', + extension: 'rar', + mimeType: 'application/x-rar-compressed', + }, + { + id: '211', + name: 'Temp.tmp', + sizeInKB: 56, + type: 'file', + extension: 'tmp', + mimeType: 'application/octet-stream', + }, + { + id: '212', + name: 'Settings.xml', + sizeInKB: 23, + type: 'file', + extension: 'xml', + mimeType: 'application/xml', + }, + { + id: '213', + name: 'Readme.md', + sizeInKB: 18, + type: 'file', + extension: 'md', + mimeType: 'text/markdown', + }, + { + id: '214', + name: 'Photo.jpg', + sizeInKB: 890, + type: 'file', + extension: 'jpg', + mimeType: 'image/jpeg', + }, + { + id: '215', + name: 'Diagram.svg', + sizeInKB: 67, + type: 'file', + extension: 'svg', + mimeType: 'image/svg+xml', + }, + { + id: '216', + name: 'Script.sh', + sizeInKB: 8, + type: 'file', + extension: 'sh', + mimeType: 'application/x-sh', + }, + { + id: '217', + name: 'Database.db', + sizeInKB: 450, + type: 'file', + extension: 'db', + mimeType: 'application/x-sqlite3', + }, + { + id: '218', + name: 'Certificate.pem', + sizeInKB: 4, + type: 'file', + extension: 'pem', + mimeType: 'application/x-pem-file', + }, + { + id: '219', + name: 'Key.key', + sizeInKB: 2, + type: 'file', + extension: 'key', + mimeType: 'application/octet-stream', + }, + ], + }, + { + id: '3', + name: 'Media', + sizeInKB: 45600, + type: 'folder', + children: [ + { + id: '30', + name: 'Vacation.mp4', + sizeInKB: 5400, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '31', + name: 'Birthday.mp4', + sizeInKB: 3200, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '32', + name: 'Wedding.mov', + sizeInKB: 8900, + type: 'file', + extension: 'mov', + mimeType: 'video/quicktime', + }, + { + id: '33', + name: 'Song1.mp3', + sizeInKB: 4500, + type: 'file', + extension: 'mp3', + mimeType: 'audio/mpeg', + }, + { + id: '34', + name: 'Song2.mp3', + sizeInKB: 3800, + type: 'file', + extension: 'mp3', + mimeType: 'audio/mpeg', + }, + { + id: '35', + name: 'Podcast.mp3', + sizeInKB: 12000, + type: 'file', + extension: 'mp3', + mimeType: 'audio/mpeg', + }, + { + id: '36', + name: 'Album-Cover.jpg', + sizeInKB: 340, + type: 'file', + extension: 'jpg', + mimeType: 'image/jpeg', + }, + { + id: '37', + name: 'Concert.mp4', + sizeInKB: 6700, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '38', + name: 'Interview.wav', + sizeInKB: 15600, + type: 'file', + extension: 'wav', + mimeType: 'audio/wav', + }, + { + id: '39', + name: 'Trailer.mp4', + sizeInKB: 2300, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '310', + name: 'Audiobook.m4a', + sizeInKB: 8900, + type: 'file', + extension: 'm4a', + mimeType: 'audio/mp4', + }, + { + id: '311', + name: 'Tutorial.mp4', + sizeInKB: 4500, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '312', + name: 'Ringtone.mp3', + sizeInKB: 450, + type: 'file', + extension: 'mp3', + mimeType: 'audio/mpeg', + }, + { + id: '313', + name: 'Voicemail.wav', + sizeInKB: 890, + type: 'file', + extension: 'wav', + mimeType: 'audio/wav', + }, + { + id: '314', + name: 'Presentation-Video.mp4', + sizeInKB: 7800, + type: 'file', + extension: 'mp4', + mimeType: 'video/mp4', + }, + { + id: '315', + name: 'Background-Music.mp3', + sizeInKB: 3400, + type: 'file', + extension: 'mp3', + mimeType: 'audio/mpeg', + }, + { + id: '316', + name: 'Sound-Effect.wav', + sizeInKB: 120, + type: 'file', + extension: 'wav', + mimeType: 'audio/wav', + }, + { + id: '317', + name: 'Animation.gif', + sizeInKB: 2300, + type: 'file', + extension: 'gif', + mimeType: 'image/gif', + }, + { + id: '318', + name: 'Thumbnail.png', + sizeInKB: 180, + type: 'file', + extension: 'png', + mimeType: 'image/png', + }, + { + id: '319', + name: 'Subtitle.srt', + sizeInKB: 45, + type: 'file', + extension: 'srt', + mimeType: 'text/plain', + }, + ], + }, + ]; + + return Promise.resolve(nodes); +}; diff --git a/examples/src/pages/tests/table/perf/flashing/default.page.tsx b/examples/src/pages/tests/table/perf/flashing/default.page.tsx new file mode 100644 index 000000000..0aabf3351 --- /dev/null +++ b/examples/src/pages/tests/table/perf/flashing/default.page.tsx @@ -0,0 +1,154 @@ +import { useRef } from 'react'; +import { + type DataSourceApi, + type InfiniteTableColumn, + InfiniteTableProps, + TreeDataSource, + TreeGrid, +} from '@infinite-table/infinite-react'; +import { type FileSystemNode, dataSource } from './data'; + +const fileIds = [ + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '110', + '111', + '112', + '113', + '114', + '115', + '116', + '117', + '118', + '119', + '20', + '21', + '22', + '23', +]; + +const columns: Record> = { + name: { + field: 'name', + renderTreeIcon: true, + header: 'Name', + style: ({ data }) => ({ + fontWeight: data?.type === 'folder' ? 'bold' : 'normal', + color: data?.type === 'folder' ? '#60a5fa' : '#d1d5db', + }), + }, + type: { + field: 'type', + header: 'Type', + render: ({ value }) => ( + + {value} + + ), + }, + extension: { + field: 'extension', + header: 'Extension', + render: ({ value }) => + value ? ( + + .{value} + + ) : null, + }, + mimeType: { + field: 'mimeType', + header: 'Mime Type', + render: ({ value }) => + value ? ( + + {value} + + ) : null, + }, + size: { + field: 'sizeInKB', + type: 'number', + header: 'Size (KB)', + render: ({ value }) => ( + 1000 ? '#f87171' : value > 500 ? '#fbbf24' : '#4ade80', + fontWeight: '600', + }} + > + {value.toLocaleString()} + + ), + }, +}; + +const domProps: InfiniteTableProps['domProps'] = { + style: { + flex: 1, + }, +}; + +export default function App() { + const apiRef = useRef>(null); + + const updateCell = () => { + const randomId = fileIds[Math.floor(Math.random() * fileIds.length)]; + apiRef.current?.updateData({ + id: randomId, + sizeInKB: Math.floor(Math.random() * 1000), + }); + }; + + return ( +
+ + (apiRef.current = api)} + > + + +
+ ); +} diff --git a/examples/src/pages/tests/table/perf/flashing/default.spec.ts b/examples/src/pages/tests/table/perf/flashing/default.spec.ts new file mode 100644 index 000000000..0ff3f1c55 --- /dev/null +++ b/examples/src/pages/tests/table/perf/flashing/default.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@testing'; + +export default test.describe.parallel('Flashing', () => { + test('perf should remain stable for 50 clicks', async ({ + page, + tracingModel, + }) => { + test.setTimeout(20000); // 50 clicks + tracing need more than default 5s + await page.waitForInfinite(); + // Wait for the button to be visible + const updateButton = page.getByRole('button', { name: 'Update Cell' }); + await expect(updateButton).toBeVisible(); + + const stop = await tracingModel.start(); + + console.log('\n Starting 50 Update Cell clicks...\n'); + + const clickCount = 50; + for (let i = 0; i < clickCount; i++) { + await updateButton.click(); + await page.waitForTimeout(20); + + if ((i + 1) % 10 === 0) { + console.log(` Progress: ${i + 1}/${clickCount} clicks completed`); + } + } + + await stop(); + }); + + test('perf should be fine when single click', async ({ + page, + tracingModel, + }) => { + await page.waitForInfinite(); + const updateButton = page.getByRole('button', { name: 'Update Cell' }); + await expect(updateButton).toBeVisible(); + + const stop = await tracingModel.start(); + + await updateButton.click(); + await stop(); + }); + + test('should be able to collapse and expand nodes with no error', async ({ + page, + tracingModel, + }) => { + await page.waitForInfinite(); + await page.failOnConsoleErrors(); + + const stop = await tracingModel.start(); + + let collapseIcon = page + .locator('svg[data-name="expand-collapse-icon"][data-state="expanded"]') + .first(); + + // collapse 2 nodes + await collapseIcon.click(); + await collapseIcon.click(); + await stop(); + }); +}); diff --git a/examples/src/pages/tests/table/props/column-pinning/virtualization.page.tsx b/examples/src/pages/tests/table/props/column-pinning/virtualization.page.tsx index 9e2dd6029..033232526 100644 --- a/examples/src/pages/tests/table/props/column-pinning/virtualization.page.tsx +++ b/examples/src/pages/tests/table/props/column-pinning/virtualization.page.tsx @@ -4,8 +4,8 @@ import { InfiniteTable, InfiniteTableApi, InfiniteTablePropColumnPinning, + DataSource, } from '@infinite-table/infinite-react'; -import { DataSource } from '@infinite-table/infinite-react'; import { rowData, Car } from '../column-visibility/rowData'; diff --git a/examples/src/pages/tests/table/props/column/column-change.spec.ts b/examples/src/pages/tests/table/props/column/column-change.spec.ts index 19ed7f159..f595273a7 100644 --- a/examples/src/pages/tests/table/props/column/column-change.spec.ts +++ b/examples/src/pages/tests/table/props/column/column-change.spec.ts @@ -6,10 +6,12 @@ export default test.describe.parallel('Column change', () => { columnModel, rowModel, headerModel, + tracingModel, }) => { await page.waitForInfinite(); await page.waitForTimeout(20); + const stop = await tracingModel.start(); let widths = ( await columnModel.getColumnWidths(['firstName', 'salary', 'stack']) ).list; @@ -42,5 +44,7 @@ export default test.describe.parallel('Column change', () => { expect(widths).toEqual([500, 100, 100]); expect(headerText).toEqual('lastName'); expect(cellText).toContain('!!!'); + + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/data/basic-add-data.spec.ts b/examples/src/pages/tests/table/props/data/basic-add-data.spec.ts index 055a7dd01..e815bb615 100644 --- a/examples/src/pages/tests/table/props/data/basic-add-data.spec.ts +++ b/examples/src/pages/tests/table/props/data/basic-add-data.spec.ts @@ -1,9 +1,17 @@ import { test, expect } from '@testing'; export default test.describe.parallel('Api', () => { - test('insert data', async ({ page, rowModel, tableModel, apiModel }) => { + test('insert data', async ({ + page, + rowModel, + tableModel, + apiModel, + tracingModel, + }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); + expect(await rowModel.getRenderedRowCount()).toEqual(8); await apiModel.evaluateDataSource((ds) => { @@ -42,5 +50,7 @@ export default test.describe.parallel('Api', () => { expect(await idCol.getValues()).toEqual( [0, 9, 10, 1, 2, 3, 4, 5, 6, 7].map(String), ); + + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/data/update2.page.tsx b/examples/src/pages/tests/table/props/data/basic-update.page.tsx similarity index 89% rename from examples/src/pages/tests/table/props/data/update2.page.tsx rename to examples/src/pages/tests/table/props/data/basic-update.page.tsx index 8bf222d15..4042dab34 100644 --- a/examples/src/pages/tests/table/props/data/update2.page.tsx +++ b/examples/src/pages/tests/table/props/data/basic-update.page.tsx @@ -28,7 +28,13 @@ type Developer = { const columns: InfiniteTablePropColumns = { // id: { field: 'id' }, - salary: { field: 'salary' }, + salary: { + field: 'salary', + header: () => { + return
Salary {Date.now()}
; + }, + defaultWidth: 250, + }, age: { field: 'age' }, firstName: { field: 'firstName' }, // preferredLanguage: { field: 'preferredLanguage' }, @@ -48,8 +54,8 @@ const dataSourceFn: DataSourceDataFn = ({}) => { setTimeout(() => { // console.log(data, 'data'); // data.length = 1; - // const newData = [...data.data.slice(0, 2)]; - const newData = data; + const newData = [...data.data.slice(0, 2)]; + // const newData = data; resolve(newData); }, 20); @@ -68,7 +74,7 @@ export default function DataTestPage() { [number, number] | null >(null); const [api, setApi] = React.useState>(); - const [header, setHeader] = React.useState(false); + const [header, setHeader] = React.useState(true); const onReady = React.useCallback( ({ @@ -107,26 +113,26 @@ export default function DataTestPage() { const { start, end } = api.getVisibleRenderRange(); const [startRow, startCol] = start; const [endRow, endCol] = end; - // console.log(start, end); const randomRow = - Math.floor(Math.random() * (endRow - startRow + 1)) + startRow; + Math.floor(Math.random() * (endRow - 1 - startRow + 1)) + + startRow; const randomCol = - Math.floor(Math.random() * (endCol - startCol + 1)) + startCol; + Math.floor(Math.random() * (endCol - 1 - startCol + 1)) + + startCol; const [activeRow, activeCol] = activeCellIndex || []; const updateRow = activeRow ?? randomRow; const updateCol = activeCol ?? randomCol; + + console.log('updating', updateRow, updateCol); const colId = Object.keys(columns)[updateCol]; const rowId = dataSourceApi.getPrimaryKeyByIndex(updateRow); dataSourceApi.updateData({ [colId]: Math.floor(Math.random() * 10000), id: rowId, }); - - // dataSourceApi.get - // dataSourceApi.; }} > update diff --git a/examples/src/pages/tests/table/props/data/basic-update.spec.ts b/examples/src/pages/tests/table/props/data/basic-update.spec.ts new file mode 100644 index 000000000..c9fac8159 --- /dev/null +++ b/examples/src/pages/tests/table/props/data/basic-update.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@testing'; + +export default test.describe.parallel('Basic data update', () => { + test('via API should not rerender header', async ({ + page, + tracingModel, + headerModel, + }) => { + await page.waitForInfinite(); + const stopTracing = await tracingModel.start(); + + const salaryHeader = headerModel.getHeaderCellLocator({ colId: 'salary' }); + const salaryInitialText = await salaryHeader.textContent(); + + const updateButton = page.getByRole('button', { + name: 'update', + exact: true, + }); + + await updateButton.click(); + + expect(await salaryHeader.textContent()).toBe(salaryInitialText); + + await stopTracing(); + }); +}); diff --git a/examples/src/pages/tests/table/props/data/edit-with-delay.spec.ts b/examples/src/pages/tests/table/props/data/edit-with-delay.spec.ts index e7143e0fe..d4e2472d0 100644 --- a/examples/src/pages/tests/table/props/data/edit-with-delay.spec.ts +++ b/examples/src/pages/tests/table/props/data/edit-with-delay.spec.ts @@ -2,8 +2,10 @@ import { test, expect } from '@testing'; export default test.describe .parallel('Immediate edit works on lazy editable columns', () => { - test('on string column', async ({ page, rowModel }) => { + test('on string column', async ({ page, rowModel, tracingModel }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); + const editor = page.locator('input'); const cell = { colId: 'firstName', @@ -38,5 +40,7 @@ export default test.describe // just wait a bit more to make sure the editor is not showing again await page.waitForTimeout(1150); expect(await editor.count()).toBe(0); + + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/data/editing/column-editor.spec.ts b/examples/src/pages/tests/table/props/data/editing/column-editor.spec.ts index b1c4f77a0..0c0561652 100644 --- a/examples/src/pages/tests/table/props/data/editing/column-editor.spec.ts +++ b/examples/src/pages/tests/table/props/data/editing/column-editor.spec.ts @@ -4,9 +4,12 @@ export default test.describe.parallel('Inline Edit', () => { page, editModel, rowModel, + tracingModel, }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); + const cellEditable1 = { colId: 'firstName', rowIndex: 0, @@ -41,5 +44,7 @@ export default test.describe.parallel('Inline Edit', () => { // make sure this second column was using default editor expect(await rowModel.getTextForCell(cellEditable2)).toBe('test'); + + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/data/editing/persistEdit.spec.ts b/examples/src/pages/tests/table/props/data/editing/persistEdit.spec.ts index e516413c5..068f1956a 100644 --- a/examples/src/pages/tests/table/props/data/editing/persistEdit.spec.ts +++ b/examples/src/pages/tests/table/props/data/editing/persistEdit.spec.ts @@ -5,9 +5,12 @@ export default test.describe.parallel('Inline Edit', () => { page, editModel, rowModel, + tracingModel, }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); + const cellEditable1 = { colId: 'firstName', rowIndex: 0, @@ -41,14 +44,18 @@ export default test.describe.parallel('Inline Edit', () => { ); expect(persistSuccessCalls).toBe(1); + + await stop(); }); test('should not persist changes to the id column', async ({ page, editModel, rowModel, + tracingModel, }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); const cellEditable1 = { colId: 'id', rowIndex: 0, @@ -70,5 +77,6 @@ export default test.describe.parallel('Inline Edit', () => { ); expect(persistErrorCalls).toBe(1); + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.page.tsx b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.page.tsx index b75dfc621..bbc4c0b6d 100644 --- a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.page.tsx +++ b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.page.tsx @@ -4,7 +4,7 @@ import { InfiniteTablePropColumns, DataSourceProps, DataSource, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; import * as React from 'react'; @@ -103,7 +103,12 @@ const dataSource: DataSourceData = ({ const spiedDataSource = sinon.spy(dataSource); function Cmp() { - (globalThis as any).state = useDataSourceState(); + const { dataArrayLength } = useDataSourceSelector((ctx) => { + return { + dataArrayLength: ctx.dataSourceState.dataArray.length, + }; + }); + (globalThis as any).dataArrayLength = dataArrayLength; return null; } diff --git a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts index 1f202f0ed..edd584112 100644 --- a/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts +++ b/examples/src/pages/tests/table/props/data/lazy-load-grouped-example.spec.ts @@ -22,7 +22,7 @@ export default test.describe.parallel('Lazy Load Grouped Data', () => { let dataSourceCalls = await getCalls({ page }); let arg = dataSourceCalls[CALL_COUNT - 1].args[0]; - expect(await page.getGlobalValue('state.dataArray.length')).toBe(20); + expect(await page.getGlobalValue('dataArrayLength')).toBe(20); expect(await getCallCount({ page })).toEqual(CALL_COUNT); expect({ diff --git a/examples/src/pages/tests/table/props/data/update.page.tsx b/examples/src/pages/tests/table/props/data/update.page.tsx index 48521af33..9f6ab586e 100644 --- a/examples/src/pages/tests/table/props/data/update.page.tsx +++ b/examples/src/pages/tests/table/props/data/update.page.tsx @@ -62,6 +62,8 @@ export default function DataTestPage() { const [ds, setDs] = React.useState>( () => dataSourceFn, ); + + const [showGrid, setShowGrid] = React.useState(true); const [dataSourceApi, setDataSourceApi] = React.useState>(); const [activeCellIndex, setActiveCellIndex] = React.useState< @@ -158,18 +160,27 @@ export default function DataTestPage() { > toggle header +
{active[0] && ( data={ds} primaryKey="id"> - - header={header} - onActiveCellIndexChange={setActiveCellIndex} - debugId="test" - onReady={onReady} - defaultActiveCellIndex={activeCellIndex} - domProps={domProps} - columns={columns} - /> + {showGrid ? ( + + header={header} + onActiveCellIndexChange={setActiveCellIndex} + debugId="test" + onReady={onReady} + defaultActiveCellIndex={activeCellIndex} + domProps={domProps} + columns={columns} + /> + ) : null} )} diff --git a/examples/src/pages/tests/table/props/filtering/filter-value/client-side-initial-filter.page.tsx b/examples/src/pages/tests/table/props/filtering/filter-value/client-side-initial-filter.page.tsx index 5478cb2f3..1fe4d939e 100644 --- a/examples/src/pages/tests/table/props/filtering/filter-value/client-side-initial-filter.page.tsx +++ b/examples/src/pages/tests/table/props/filtering/filter-value/client-side-initial-filter.page.tsx @@ -3,7 +3,7 @@ import { InfiniteTable, DataSource, DataSourcePropFilterValue, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; @@ -59,7 +59,11 @@ const domProps: React.HTMLAttributes = { }; function UnfilterdCount() { - const { unfilteredCount } = useDataSourceState(); + const { unfilteredCount } = useDataSourceSelector((ctx) => { + return { + unfilteredCount: ctx.dataSourceState.unfilteredCount, + }; + }); return (

unfiltered count: {unfilteredCount} diff --git a/examples/src/pages/tests/table/props/filtering/filter-value/client-side.page.tsx b/examples/src/pages/tests/table/props/filtering/filter-value/client-side.page.tsx index cc65d6a3a..4cf8015ee 100644 --- a/examples/src/pages/tests/table/props/filtering/filter-value/client-side.page.tsx +++ b/examples/src/pages/tests/table/props/filtering/filter-value/client-side.page.tsx @@ -3,7 +3,7 @@ import { InfiniteTable, DataSource, DataSourcePropFilterValue, - useDataSourceState, + useDataSourceSelector, } from '@infinite-table/infinite-react'; import * as React from 'react'; import { useState } from 'react'; @@ -54,7 +54,11 @@ const domProps: React.HTMLAttributes = { }; function UnfilterdCount() { - const { unfilteredCount } = useDataSourceState(); + const { unfilteredCount } = useDataSourceSelector((ctx) => { + return { + unfilteredCount: ctx.dataSourceState.unfilteredCount, + }; + }); return (

unfiltered count: {unfilteredCount} diff --git a/examples/src/pages/tests/table/props/filtering/filter-value/client-side.spec.ts b/examples/src/pages/tests/table/props/filtering/filter-value/client-side.spec.ts index 29d08d0f0..961a5d43d 100644 --- a/examples/src/pages/tests/table/props/filtering/filter-value/client-side.spec.ts +++ b/examples/src/pages/tests/table/props/filtering/filter-value/client-side.spec.ts @@ -9,9 +9,10 @@ import { } from './filterFn'; export default test.describe.parallel('Client side filterFunction', () => { - test('Filters correctly', async ({ page }) => { + test('Filters correctly', async ({ page, tracingModel }) => { await page.waitForInfinite(); + const stop = await tracingModel.start(); expect(await getRowCount({ page })).toEqual(employees.length); const unfiltered = page.getByLabel('unfiltered-count'); @@ -36,5 +37,7 @@ export default test.describe.parallel('Client side filterFunction', () => { await page.click('button[data-name="none"]'); expect(await getRowCount({ page })).toEqual(employees.length); expect(await unfiltered.textContent()).toEqual(expectedUnfilteredCount); + + await stop(); }); }); diff --git a/examples/src/pages/tests/table/props/group-by/update-and-rerender.page.tsx b/examples/src/pages/tests/table/props/group-by/update-and-rerender.page.tsx new file mode 100644 index 000000000..bc5811247 --- /dev/null +++ b/examples/src/pages/tests/table/props/group-by/update-and-rerender.page.tsx @@ -0,0 +1,95 @@ +import { + InfiniteTableColumn, + InfiniteTable, + DataSource, + DataSourcePropAggregationReducers, +} from '@infinite-table/infinite-react'; +import * as React from 'react'; + +import { data, Person } from './people'; + +const columns: Record> = { + name: { + field: 'name', + }, + country: { + field: 'country', + }, + department: { + field: 'department', + }, + age: { + field: 'age', + type: 'number', + defaultEditable: true, + renderValue: ({ value }) => { + return `${value}-${Date.now()}`; + }, + header: () => { + return

Age-{Date.now()}
; + }, + getValueToPersist: ({ value }) => { + return Number(value); + }, + }, + salary: { + field: 'salary', + type: 'number', + + render: ({ value, rowInfo }) => { + if (rowInfo.isGroupRow) { + return ( + <> + Avg salary {rowInfo.groupKeys?.join(', ')}:{' '} + {rowInfo.reducerResults![0]} + + ); + } + return <>{value}; + }, + }, + team: { + field: 'team', + }, +}; +const columnAggregations: DataSourcePropAggregationReducers = { + salary: { + initialValue: 0, + field: 'salary', + reducer: (acc, sum) => acc + sum, + done: (sum, arr) => (arr.length ? sum / arr.length : 0), + }, + age: { + initialValue: 0, + field: 'age', + reducer: (acc, sum) => acc + sum, + done: (sum, arr) => + arr.length ? Math.round((sum / arr.length) * 100) / 100 : 0, + }, +}; + +export default function GroupByExample() { + return ( + + + data={data} + primaryKey="id" + groupBy={[{ field: 'department' }, { field: 'team' }]} + aggregationReducers={columnAggregations} + > + + domProps={{ + style: { + margin: '5px', + height: '80vh', + border: '1px solid gray', + position: 'relative', + }, + }} + columnDefaultWidth={150} + columns={columns} + /> + + + ); +} diff --git a/examples/src/pages/tests/table/props/group-by/update-and-rerender.spec.ts b/examples/src/pages/tests/table/props/group-by/update-and-rerender.spec.ts new file mode 100644 index 000000000..dd8cd76cd --- /dev/null +++ b/examples/src/pages/tests/table/props/group-by/update-and-rerender.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@testing'; + +export default test.describe.parallel('Group By rerender on update', () => { + test('should work correctly', async ({ + page, + + tableModel, + editModel, + }) => { + await page.waitForInfinite(); + + const ageColumn = tableModel.withColumn('age'); + + const headerText = await ageColumn.getHeader(); + + const ageRenderedValues = await ageColumn.getValues(); + + const cellLocation = { + colId: 'age', + rowIndex: 2, + }; + + await editModel.startEdit({ + event: 'dblclick', + ...cellLocation, + value: '23', + }); + + await editModel.confirmEdit(cellLocation); + + const ageRenderedValuesAfter = await ageColumn.getValues(); + + // the first 3 rows should have been rerendered + expect(ageRenderedValuesAfter[0]).not.toEqual(ageRenderedValues[0]); + expect(ageRenderedValuesAfter[1]).not.toEqual(ageRenderedValues[1]); + expect(ageRenderedValuesAfter[2]).not.toEqual(ageRenderedValues[2]); + + // the rest remain the same + expect(ageRenderedValuesAfter.slice(3)).toEqual(ageRenderedValues.slice(3)); + + // value before edit + expect(ageRenderedValues[1]?.split('-')[0]).toEqual('20'); + // value after edit + expect(ageRenderedValuesAfter[1]?.split('-')[0]).toEqual('21'); + + expect(await ageColumn.getHeader()).toEqual(headerText); + }); + + test('header should update when resizing column', async ({ + page, + tableModel, + }) => { + await page.waitForInfinite(); + + const ageColumn = tableModel.withColumn('age'); + + const headerText = await ageColumn.getHeader(); + + await ageColumn.resize(100); + + expect(await ageColumn.getHeader()).not.toEqual(headerText); + }); +}); diff --git a/examples/src/pages/tests/testUtils/TracingModel.ts b/examples/src/pages/tests/testUtils/TracingModel.ts index f9d00da56..8dadc527a 100644 --- a/examples/src/pages/tests/testUtils/TracingModel.ts +++ b/examples/src/pages/tests/testUtils/TracingModel.ts @@ -112,7 +112,11 @@ export class TracingModel { // Only save baseline when total time is lower than previous (or no baseline exists) const existingBaseline = getBaseline(testName); - if (!existingBaseline || metrics[compare] < existingBaseline[compare]) { + if ( + !existingBaseline || + metrics[compare] < + existingBaseline[compare] * (1 - existingBaseline.threshold / 100) + ) { saveBaseline(testName, metrics); console.log( `📝 Updated ${isCI ? 'CI' : 'local'} baseline: ${ diff --git a/examples/test-fixtures.ts b/examples/test-fixtures.ts index 130d7a38d..b77a6d87d 100644 --- a/examples/test-fixtures.ts +++ b/examples/test-fixtures.ts @@ -73,6 +73,7 @@ export const test = base.extend< } >({ tracingModel: async ({ page, browser }, use, testInfo) => { + testInfo.setTimeout(10_000); const { filePathNoExt } = getFileInfoFromTestInfo(testInfo); await use( diff --git a/source/src/components/DataSource/DataSourceCmp.tsx b/source/src/components/DataSource/DataSourceCmp.tsx index 069a1cdd0..38b7b61fb 100644 --- a/source/src/components/DataSource/DataSourceCmp.tsx +++ b/source/src/components/DataSource/DataSourceCmp.tsx @@ -4,16 +4,15 @@ import { useEffect, useLayoutEffect } from 'react'; import { useManagedComponentState } from '../hooks/useComponentState'; import { useLatest } from '../hooks/useLatest'; -import { getDataSourceContext } from './DataSourceContext'; +import { getDataSourceStoreContext } from './DataSourceContext'; +import { createDataSourceStore } from './DataSourceStore'; import { getDataSourceApi } from './getDataSourceApi'; import { useLoadData } from './privateHooks/useLoadData'; -import { - useDataSourceContextValue, - useMasterDetailContext, -} from './publicHooks/useDataSourceState'; +import { useGetMasterDetailContext } from './publicHooks/useDataSourceMasterDetailSelector'; import { DataSourceContextValue, DataSourceState } from './types'; +import { useDataSourceSelector } from './publicHooks/useDataSourceSelector'; export type DataSourceChildren = | React.ReactNode @@ -22,14 +21,21 @@ export type DataSourceChildren = function DataSourceWithContext(props: { children: DataSourceChildren }) { let { children } = props; - const { api, componentState } = useDataSourceContextValue(); - + const { dataSourceApi, dataSourceState, onReady } = useDataSourceSelector( + (ctx) => { + return { + dataSourceApi: ctx.dataSourceApi, + dataSourceState: ctx.dataSourceState, + onReady: ctx.dataSourceState.onReady, + }; + }, + ); if (typeof children === 'function') { - children = children(componentState); + children = children(dataSourceState); } useEffect(() => { - componentState.onReady?.(api); + onReady?.(dataSourceApi); }, []); return <>{children}; @@ -41,10 +47,11 @@ export function DataSourceCmp({ children: DataSourceChildren; isDetail: boolean; }) { - const DataSourceContext = getDataSourceContext(); + const DataSourceStoreContext = getDataSourceStoreContext(); + + const [store] = React.useState(() => createDataSourceStore()); - const masterContext = useMasterDetailContext(); - const getDataSourceMasterContext = useLatest(masterContext); + const getDataSourceMasterContext = useGetMasterDetailContext(); const { componentState, componentActions, assignState } = useManagedComponentState>(); @@ -58,15 +65,16 @@ export function DataSourceCmp({ return getDataSourceApi({ getState, actions: componentActions }); }); const contextValue: DataSourceContextValue = { - componentState, - componentActions, + dataSourceState: componentState, + dataSourceActions: componentActions, getDataSourceMasterContext, - getState, + getDataSourceState: getState, assignState, - api, + dataSourceApi: api, }; useLayoutEffect(() => { + const masterContext = getDataSourceMasterContext(); if (masterContext) { masterContext.registerDetail(contextValue); } @@ -93,10 +101,10 @@ export function DataSourceCmp({ }; } - useLoadData({ - componentActions, - componentState, - getComponentState: getState, + useLoadData({ + dataSourceActions: componentActions, + dataSourceState: componentState, + getDataSourceState: getState, }); useEffect(() => { @@ -122,9 +130,15 @@ export function DataSourceCmp({ } }, [componentState.originalDataArrayChangedInfo]); + store.setSnapshot(contextValue); + + React.useLayoutEffect(() => { + store.notify(); + }); + return ( - + - + ); } diff --git a/source/src/components/DataSource/DataSourceContext.ts b/source/src/components/DataSource/DataSourceContext.ts index 594301cc9..fea8d6ebc 100644 --- a/source/src/components/DataSource/DataSourceContext.ts +++ b/source/src/components/DataSource/DataSourceContext.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { DataSourceApi, DataSourceState } from '.'; import { DataSourceComponentActions, DataSourceContextValue } from './types'; +import type { DataSourceStore } from './DataSourceStore'; let DSContext: any; @@ -13,11 +14,25 @@ export function getDataSourceContext(): React.Context< } return (DSContext = React.createContext>({ - api: null as any as DataSourceApi, - getState: () => null as any as DataSourceState, + dataSourceApi: null as any as DataSourceApi, + getDataSourceState: () => null as any as DataSourceState, assignState: () => null as any as DataSourceState, getDataSourceMasterContext: () => undefined, - componentState: null as any as DataSourceState, - componentActions: null as any as DataSourceComponentActions, + dataSourceState: null as any as DataSourceState, + dataSourceActions: null as any as DataSourceComponentActions, })); } + +let DSStoreContext: any; + +export function getDataSourceStoreContext(): React.Context< + DataSourceStore +> { + if (DSStoreContext as React.Context>) { + return DSStoreContext; + } + + return (DSStoreContext = React.createContext>( + null as any as DataSourceStore, + )); +} diff --git a/source/src/components/DataSource/DataSourceMasterDetailContext.ts b/source/src/components/DataSource/DataSourceMasterDetailContext.ts index 8b45b0d1c..1bb13043b 100644 --- a/source/src/components/DataSource/DataSourceMasterDetailContext.ts +++ b/source/src/components/DataSource/DataSourceMasterDetailContext.ts @@ -1,21 +1,19 @@ import * as React from 'react'; -import { DataSourceMasterDetailContextValue } from './types'; +import { DataSourceMasterDetailStore } from './DataSourceMasterDetailStore'; -let DSMasterDetailContext: any; +let DSMasterDetailStoreContext: any; -export function getDataSourceMasterDetailContext(): React.Context< - DataSourceMasterDetailContextValue | undefined +export function getDataSourceMasterDetailStoreContext(): React.Context< + DataSourceMasterDetailStore > { if ( - DSMasterDetailContext as React.Context< - DataSourceMasterDetailContextValue | undefined - > + DSMasterDetailStoreContext as React.Context> ) { - return DSMasterDetailContext; + return DSMasterDetailStoreContext; } - return (DSMasterDetailContext = React.createContext< - DataSourceMasterDetailContextValue | undefined - >(undefined)); + return (DSMasterDetailStoreContext = React.createContext< + DataSourceMasterDetailStore + >(null as any as DataSourceMasterDetailStore)); } diff --git a/source/src/components/DataSource/DataSourceMasterDetailStore.ts b/source/src/components/DataSource/DataSourceMasterDetailStore.ts new file mode 100644 index 000000000..853baefa1 --- /dev/null +++ b/source/src/components/DataSource/DataSourceMasterDetailStore.ts @@ -0,0 +1,14 @@ +import { + ComponentStore, + createComponentStore, +} from '../../utils/ComponentStore'; +import { DataSourceMasterDetailContextValue } from './types'; + +export interface DataSourceMasterDetailStore + extends ComponentStore> {} + +export function createDataSourceMasterDetailStore() { + return createComponentStore< + DataSourceMasterDetailContextValue + >() as DataSourceMasterDetailStore; +} diff --git a/source/src/components/DataSource/DataSourceStore.ts b/source/src/components/DataSource/DataSourceStore.ts new file mode 100644 index 000000000..50391926c --- /dev/null +++ b/source/src/components/DataSource/DataSourceStore.ts @@ -0,0 +1,14 @@ +import { + ComponentStore, + createComponentStore, +} from '../../utils/ComponentStore'; +import type { DataSourceContextValue } from './types'; + +export interface DataSourceStore + extends ComponentStore> {} + +export function createDataSourceStore() { + return createComponentStore< + DataSourceContextValue + >() as DataSourceStore; +} diff --git a/source/src/components/DataSource/index.tsx b/source/src/components/DataSource/index.tsx index a22287e65..0c2d03780 100644 --- a/source/src/components/DataSource/index.tsx +++ b/source/src/components/DataSource/index.tsx @@ -7,10 +7,7 @@ import { defaultFilterTypes } from './defaultFilterTypes'; import { GroupRowsState } from './GroupRowsState'; -import { - useDataSourceState, - useMasterDetailContext, -} from './publicHooks/useDataSourceState'; +import { useDataSourceState } from './publicHooks/useDataSourceState'; import { RowSelectionState } from './RowSelectionState'; import { CellSelectionState } from './CellSelectionState'; import { @@ -24,11 +21,12 @@ import { } from './state/getInitialState'; import { concludeReducer, filterDataArray } from './state/reducer'; -import { InfiniteTableRowInfo } from '../InfiniteTable'; // import { DataSourceCmp } from './DataSourceCmp'; import { useDataSourceInternal } from './privateHooks/useDataSource'; import { DataSourceProps } from './types'; import { RowDisabledState } from './RowDisabledState'; +import { useDataSourceSelector } from './publicHooks/useDataSourceSelector'; +import { useMasterRowInfo } from './publicHooks/useDataSourceMasterDetailSelector'; const { // ManagedComponentContextProvider: ManagedDataSourceContextProvider, @@ -61,23 +59,18 @@ function DataSource(props: DataSourceProps) { // TODO document this function useRowInfoReducers() { - const { rowInfoReducerResults } = useDataSourceState(); + const { rowInfoReducerResults } = useDataSourceSelector((ctx) => { + return { + rowInfoReducerResults: ctx.dataSourceState.rowInfoReducerResults, + }; + }); return rowInfoReducerResults; } -function useMasterRowInfo(): InfiniteTableRowInfo | undefined { - const context = useMasterDetailContext(); - - if (!context) { - return undefined; - } - - return context.masterRowInfo as InfiniteTableRowInfo; -} - export { useManagedDataSource, + useDataSourceSelector, useDataSourceState, DataSource, GroupRowsState, diff --git a/source/src/components/DataSource/privateHooks/useDataSource.tsx b/source/src/components/DataSource/privateHooks/useDataSource.tsx index 55d80ef4e..f25e97a88 100644 --- a/source/src/components/DataSource/privateHooks/useDataSource.tsx +++ b/source/src/components/DataSource/privateHooks/useDataSource.tsx @@ -14,18 +14,25 @@ import { import { ManagedComponentStateContextValue } from '../../hooks/useComponentState/types'; import { useLatest } from '../../hooks/useLatest'; import { DataSourceChildren } from '../DataSourceCmp'; -import { getDataSourceContext } from '../DataSourceContext'; +import { + getDataSourceContext, + getDataSourceStoreContext, +} from '../DataSourceContext'; +import { createDataSourceStore } from '../DataSourceStore'; import { getDataSourceApi } from '../getDataSourceApi'; import { useLoadData } from './useLoadData'; -import { useMasterDetailContext } from '../publicHooks/useDataSourceState'; +import { + useMasterRowInfo, + useGetMasterDetailContext, +} from '../publicHooks/useDataSourceMasterDetailSelector'; export function useDataSourceInternal>( props: Omit, ) { - const masterContext = useMasterDetailContext(); - const getDataSourceMasterContext = useLatest(masterContext); + const masterRowInfo = useMasterRowInfo(); + const getDataSourceMasterContext = useGetMasterDetailContext(); - const isDetail = !!masterContext; + const isDetail = !!masterRowInfo; // when we are in a detail DataSource, we want to have a key // dependent on the master row info // since we dont want to recycle and reuse the DataSource of a detail row @@ -33,33 +40,39 @@ export function useDataSourceInternal>( // while having more details expanded) // so making sure the key is unique for each detail row is important // and mandatory to ensure correctness - const key = isDetail ? masterContext.masterRowInfo.id : 'master'; + const key = isDetail ? masterRowInfo.id : 'master'; const { contextValue: managedContextValue, ContextComponent } = useManagedDataSource(props); - const { componentActions, componentState, assignState } = - managedContextValue as any as ManagedComponentStateContextValue< - DataSourceState, - DataSourceComponentActions - >; + const { + componentActions: dataSourceActions, + componentState: dataSourceState, + assignState, + } = managedContextValue as any as ManagedComponentStateContextValue< + DataSourceState, + DataSourceComponentActions + >; - componentState.getDataSourceMasterContextRef.current = + dataSourceState.getDataSourceMasterContextRef.current = getDataSourceMasterContext; - const getState = useLatest(componentState); + const getDataSourceState = useLatest(dataSourceState); - const [api] = useState(() => - getDataSourceApi({ getState, actions: componentActions }), + const [dataSourceApi] = useState(() => + getDataSourceApi({ + getState: getDataSourceState, + actions: dataSourceActions, + }), ); const contextValue: DataSourceContextValue = { - componentState, - componentActions, + dataSourceState, + dataSourceActions, getDataSourceMasterContext, - getState, + getDataSourceState, assignState, - api, + dataSourceApi, }; const getLatestManagedContextValue = useLatest(managedContextValue); @@ -69,16 +82,30 @@ export function useDataSourceInternal>( const DataSource = useCallback( ({ children }: { children: DataSourceChildren; nodesKey?: string }) => { + const DataSourceStoreContext = getDataSourceStoreContext(); + const [store] = useState(() => createDataSourceStore()); + if (typeof children === 'function') { - children = children(getState()); + children = children(getDataSourceState()); } + + const contextValue = getLatestContextValue(); + + store.setSnapshot(contextValue); + + useLayoutEffect(() => { + store.notify(); + }); + return ( - - {children} + + + {children} + ); @@ -87,6 +114,7 @@ export function useDataSourceInternal>( ); useLayoutEffect(() => { + const masterContext = getDataSourceMasterContext(); if (masterContext) { masterContext.registerDetail(contextValue); } @@ -94,74 +122,76 @@ export function useDataSourceInternal>( useLayoutEffect(() => { return () => { - const state = getState(); + const state = getDataSourceState(); state.onCleanup(state); }; }, []); if (__DEV__ && !isDetail) { - (globalThis as any).getDataSourceState = getState; - (globalThis as any).dataSourceActions = componentActions; - (globalThis as any).dataSourceApi = api; + (globalThis as any).getDataSourceState = getDataSourceState; + (globalThis as any).dataSourceActions = dataSourceActions; + (globalThis as any).dataSourceApi = dataSourceApi; } - if (__DEV__ && componentState.debugId) { + if (__DEV__ && dataSourceState.debugId) { (globalThis as any).dataSources = (globalThis as any).dataSources || {}; - (globalThis as any)['dataSources'][componentState.debugId] = { - getState, - actions: componentActions, - api, + (globalThis as any)['dataSources'][dataSourceState.debugId] = { + getState: getDataSourceState, + actions: dataSourceActions, + api: dataSourceApi, }; } useLoadData({ - componentState, - componentActions, - getComponentState: getState, + dataSourceState, + dataSourceActions, + getDataSourceState, }); useEffect(() => { - componentState.onDataArrayChange?.( - componentState.originalDataArray, - componentState.originalDataArrayChangedInfo, + dataSourceState.onDataArrayChange?.( + dataSourceState.originalDataArray, + dataSourceState.originalDataArrayChangedInfo, ); if ( - componentState.onDataMutations && - componentState.originalDataArrayChangedInfo.mutations && - componentState.originalDataArrayChangedInfo.mutations.size + dataSourceState.onDataMutations && + dataSourceState.originalDataArrayChangedInfo.mutations && + dataSourceState.originalDataArrayChangedInfo.mutations.size ) { - componentState.onDataMutations({ + dataSourceState.onDataMutations({ primaryKeyField: - typeof componentState.primaryKey === 'string' - ? componentState.primaryKey + typeof dataSourceState.primaryKey === 'string' + ? dataSourceState.primaryKey : undefined, - dataArray: componentState.originalDataArray, - mutations: componentState.originalDataArrayChangedInfo.mutations, - timestamp: componentState.originalDataArrayChangedInfo.timestamp, + dataArray: dataSourceState.originalDataArray, + mutations: dataSourceState.originalDataArrayChangedInfo.mutations, + timestamp: dataSourceState.originalDataArrayChangedInfo.timestamp, }); } if ( - componentState.onTreeDataMutations && - componentState.originalDataArrayChangedInfo.treeMutations && - componentState.originalDataArrayChangedInfo.treeMutations.size + dataSourceState.onTreeDataMutations && + dataSourceState.originalDataArrayChangedInfo.treeMutations && + dataSourceState.originalDataArrayChangedInfo.treeMutations.size ) { - componentState.onTreeDataMutations({ - nodesKey: componentState.nodesKey ? componentState.nodesKey : undefined, - dataArray: componentState.originalDataArray, + dataSourceState.onTreeDataMutations({ + nodesKey: dataSourceState.nodesKey + ? dataSourceState.nodesKey + : undefined, + dataArray: dataSourceState.originalDataArray, treeMutations: - componentState.originalDataArrayChangedInfo.treeMutations, - timestamp: componentState.originalDataArrayChangedInfo.timestamp, + dataSourceState.originalDataArrayChangedInfo.treeMutations, + timestamp: dataSourceState.originalDataArrayChangedInfo.timestamp, }); } - }, [componentState.originalDataArrayChangedInfo]); + }, [dataSourceState.originalDataArrayChangedInfo]); useEffect(() => { - componentState.onReady?.(api); + dataSourceState.onReady?.(dataSourceApi); }, []); return { DataSource, - state: contextValue.componentState, + state: contextValue.dataSourceState, }; } diff --git a/source/src/components/DataSource/privateHooks/useLoadData.ts b/source/src/components/DataSource/privateHooks/useLoadData.ts index c64f77283..b8d920b6c 100644 --- a/source/src/components/DataSource/privateHooks/useLoadData.ts +++ b/source/src/components/DataSource/privateHooks/useLoadData.ts @@ -16,12 +16,11 @@ import { LAZY_ROOT_KEY_FOR_GROUPS } from '../../../utils/groupAndPivot'; import { raf } from '../../../utils/raf'; import { ComponentStateGeneratedActions } from '../../hooks/useComponentState/types'; import { useEffectWithChanges } from '../../hooks/useEffectWithChanges'; -import { useLatest } from '../../hooks/useLatest'; import { Scrollbars } from '../../InfiniteTable'; import { assignExcept } from '../../InfiniteTable/utils/assignFiltered'; import { debounce } from '../../utils/debounce'; import type { RenderRange } from '../../VirtualBrain'; -import { useMasterDetailContext } from '../publicHooks/useDataSourceState'; +import { useGetMasterDetailContext } from '../publicHooks/useDataSourceMasterDetailSelector'; import { cleanupEmptyFilterValues } from '../state/reducer'; import { DataSourceDataParams, @@ -84,7 +83,7 @@ export function buildDataSourceDataParams( componentState: DataSourceStateForDataParams, overrides?: Partial>, masterContext?: { - masterRowInfo: DataSourceMasterDetailContextValue['masterRowInfo']; + masterRowInfo: DataSourceMasterDetailContextValue['masterRowInfo']; }, ): DataSourceDataParams { const sortInfo = componentState.multiSort @@ -175,7 +174,7 @@ export function loadData( componentState: DataSourceState, actions: ComponentStateGeneratedActions>, overrides?: Partial>, - masterContext?: DataSourceMasterDetailContextValue | undefined, + masterContext?: DataSourceMasterDetailContextValue | undefined, ) { const dataParams = buildDataSourceDataParams( componentState, @@ -468,16 +467,12 @@ function getDetailReady( } type LoadDataOptions = { - componentActions: DataSourceComponentActions; - componentState: DataSourceState; - getComponentState: () => DataSourceState; + dataSourceActions: DataSourceComponentActions; + dataSourceState: DataSourceState; + getDataSourceState: () => DataSourceState; }; export function useLoadData(options: LoadDataOptions) { - const { - getComponentState, - componentActions: actions, - componentState, - } = options; + const { getDataSourceState, dataSourceActions, dataSourceState } = options; const { data, @@ -494,7 +489,7 @@ export function useLoadData(options: LoadDataOptions) { livePaginationCursor, filterTypes, cursorId: stateCursorId, - } = componentState; + } = dataSourceState; const [scrollbars, setScrollbars] = useState({ vertical: false, @@ -526,7 +521,7 @@ export function useLoadData(options: LoadDataOptions) { // this line makes it so that when we have live pagination, with a livePaginationCursor, // if the data that was loaded does not fill the whole viewport, we need to keep requesting the new // batch of data - so this assignment here does that - actions.cursorId = livePaginationCursor; + dataSourceActions.cursorId = livePaginationCursor; } } }); @@ -548,7 +543,7 @@ export function useLoadData(options: LoadDataOptions) { // #useDataArrayLengthAsCursor ref if (stateCursorId != null && dataArray.length) { - actions.cursorId = dataArray.length; + dataSourceActions.cursorId = dataArray.length; } } }); @@ -557,7 +552,7 @@ export function useLoadData(options: LoadDataOptions) { }, [dataArray.length, livePaginationCursor]); useEffect(() => { - const state = getComponentState(); + const state = getDataSourceState(); const { livePaginationCursor, livePagination, dataArray } = state; @@ -570,13 +565,13 @@ export function useLoadData(options: LoadDataOptions) { if (livePaginationCursor) { // only do this if livePaginationCursor is defined and not zero - actions.cursorId = livePaginationCursor; + dataSourceActions.cursorId = livePaginationCursor; } else if (livePaginationCursor === undefined && dataArray.length) { // there is no cursor passed as a prop, so we use dataArray.length as a cursor // so only do this if the length > 0 // #useDataArrayLengthAsCursor ref - actions.cursorId = dataArray.length; + dataSourceActions.cursorId = dataArray.length; } } }, [scrollbars.vertical]); @@ -609,17 +604,17 @@ export function useLoadData(options: LoadDataOptions) { cursorId: livePagination ? stateCursorId : null, }); - const getMasterContext = useLatest(useMasterDetailContext()); + const getMasterContext = useGetMasterDetailContext(); const dataChangeTimestampsRef = useRef([]); useEffectWithChanges( () => { - const componentState = getComponentState(); + const componentState = getDataSourceState(); const masterContext = getMasterContext(); const { isDetail, isDetailReady } = getDetailReady( masterContext, - getComponentState, + getDataSourceState, ); if (isDetail && !isDetailReady) { return; @@ -646,7 +641,7 @@ export function useLoadData(options: LoadDataOptions) { loadData( componentState.data, componentState, - actions, + dataSourceActions, undefined, masterContext, ); @@ -663,13 +658,16 @@ export function useLoadData(options: LoadDataOptions) { if (keys.length === 1) { appendWhenLivePagination = !!changes.cursorId; - if (changes.filterValue && getComponentState().filterMode === 'local') { + if ( + changes.filterValue && + getDataSourceState().filterMode === 'local' + ) { // if filter value has changed and filter mode is local // then we don't need to do a remote call return; } - const originalData = getComponentState().data; + const originalData = getDataSourceState().data; if (Array.isArray(originalData) && changes.refetchKey) { // the data is an array, but the refetchKey has changed // so let's assign originalDataArray to the data array @@ -678,7 +676,7 @@ export function useLoadData(options: LoadDataOptions) { // because it's needed in a edge case that's not easy to reproduce //@ts-ignore ignore - actions.originalDataArray = originalData; + dataSourceActions.originalDataArray = originalData; return; } } @@ -686,14 +684,14 @@ export function useLoadData(options: LoadDataOptions) { const masterContext = getMasterContext(); const { isDetail, isDetailReady } = getDetailReady( masterContext, - getComponentState, + getDataSourceState, ); if (isDetail && !isDetailReady) { return; } - const componentState = getComponentState(); + const componentState = getDataSourceState(); if (typeof componentState.data === 'function') { let marker: DevToolsMarker | undefined; @@ -705,7 +703,7 @@ export function useLoadData(options: LoadDataOptions) { loadData( componentState.data, componentState, - actions, + dataSourceActions, { append: appendWhenLivePagination, }, @@ -720,16 +718,18 @@ export function useLoadData(options: LoadDataOptions) { // only for initial triggering `onDataParamsChange` useEffectWithChanges(() => { - const componentState = getComponentState(); + const componentState = getDataSourceState(); if (initialRef.current) { initialRef.current = false; const dataParams = buildDataSourceDataParams( componentState, undefined, - getMasterContext(), + getMasterContext() as + | DataSourceMasterDetailContextValue + | undefined, ); - actions.dataParams = dataParams; + dataSourceActions.dataParams = dataParams; } }, depsObject); } @@ -746,15 +746,14 @@ function useLazyLoadRange( options: LoadDataOptions, dependencies: LazyLoadDeps, ) { - const { - getComponentState, - componentActions: actions, - componentState, - } = options; + const { getDataSourceState, dataSourceActions, dataSourceState } = options; useEffect(() => { - actions.lazyLoadCacheOfLoadedBatches = new DeepMap(); - }, [componentState.data, componentState.dataParams]); + dataSourceActions.lazyLoadCacheOfLoadedBatches = new DeepMap< + string, + true + >(); + }, [dataSourceState.data, dataSourceState.dataParams]); // const loadingCache = useMemo>(() => { // return new Map(); @@ -768,17 +767,17 @@ function useLazyLoadRange( dataArray, groupRowsState, scrollStopDelayUpdatedByTable, - } = componentState; + } = dataSourceState; const latestRenderRangeRef = useRef(null); const loadRange = ( renderRange?: RenderRange | null, options?: { dismissLoadedRows?: boolean }, - cache: DeepMap = getComponentState() + cache: DeepMap = getDataSourceState() .lazyLoadCacheOfLoadedBatches, ) => { - const componentState = getComponentState(); + const componentState = getDataSourceState(); renderRange = renderRange || latestRenderRangeRef.current; if (!renderRange) { return; @@ -811,7 +810,7 @@ function useLazyLoadRange( endIndex, lazyLoadBatchSize, componentState, - componentActions: actions, + componentActions: dataSourceActions, dismissLoadedRows: options?.dismissLoadedRows ?? false, }, cache, @@ -837,13 +836,13 @@ function useLazyLoadRange( // clear the cache of loaded batches // as the changes in sorting/filtering/grouping/pivoting // need to reload new data, but they won't if we don't clear the cache - getComponentState().lazyLoadCacheOfLoadedBatches.clear(); + getDataSourceState().lazyLoadCacheOfLoadedBatches.clear(); // it's crucial to also clear the originalLazyGroupData // as otherwise, previously loaded data will be kept in memory // eg - from another sort/group configuration // see #make-sure-old-lazy-data-is-cleared - getComponentState().originalLazyGroupData.clear(); + getDataSourceState().originalLazyGroupData.clear(); // loadRange(notifyRenderRangeChange.get(), { dismissLoadedRows: true, diff --git a/source/src/components/DataSource/publicHooks/useDataSourceActions.ts b/source/src/components/DataSource/publicHooks/useDataSourceActions.ts deleted file mode 100644 index 30091c04b..000000000 --- a/source/src/components/DataSource/publicHooks/useDataSourceActions.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -import { DataSourceComponentActions } from '../types'; - -import { getDataSourceContext } from '../DataSourceContext'; - -export default function useDataSourceActions< - T, ->(): DataSourceComponentActions { - const DataSourceContext = getDataSourceContext(); - const contextValue = React.useContext(DataSourceContext); - - return contextValue.componentActions; -} diff --git a/source/src/components/DataSource/publicHooks/useDataSourceMasterDetailSelector.ts b/source/src/components/DataSource/publicHooks/useDataSourceMasterDetailSelector.ts new file mode 100644 index 000000000..0be0a517b --- /dev/null +++ b/source/src/components/DataSource/publicHooks/useDataSourceMasterDetailSelector.ts @@ -0,0 +1,118 @@ +import { useCallback, useContext } from 'react'; + +import type { DataSourceMasterDetailContextValue } from '../types'; + +import { + createComponentStore, + useComponentStoreSelector, + useComponentStoreSingleValue, +} from '../../../utils/ComponentStore'; +import { getDataSourceMasterDetailStoreContext } from '../DataSourceMasterDetailContext'; +import { DataSourceMasterDetailStore } from '../DataSourceMasterDetailStore'; +import { InfiniteTableRowInfo } from '../../InfiniteTable'; + +/** + * A stable no-op store used as a fallback so that hooks are called + * unconditionally (Rules of Hooks) even when outside a detail context. + */ +const NOOP_STORE = createComponentStore(); +const NOOP_SELECTOR = () => undefined; + +/** + * Returns the raw master-detail store (or undefined when outside a detail row). + * + * Use this when you need on-demand access to the master-detail context + * (e.g. inside effects or callbacks) without subscribing the component + * to re-renders. Call `store.getSnapshot()` to read the current value. + */ +export function useMasterDetailStore(): + | DataSourceMasterDetailStore + | undefined { + const StoreContext = getDataSourceMasterDetailStoreContext(); + const store = useContext(StoreContext) as DataSourceMasterDetailStore; + + return store || undefined; +} + +/** + * Returns a stable getter `() => DataSourceMasterDetailContextValue | undefined` + * that always returns the latest master-detail context snapshot. + * + * This is the store-based replacement for the old + * `useLatest(useMasterDetailContext())` pattern: the component does NOT + * re-render when the master-detail context changes, but calling the + * returned function will always give you the most recent value. + */ +export function useGetMasterDetailContext(): () => + | DataSourceMasterDetailContextValue + | undefined { + const store = useMasterDetailStore(); + + return useCallback(() => store?.getSnapshot(), [store]); +} + +/** + * A selector hook that reads a single value from the DataSource store. + * Uses Object.is for equality — ideal for primitives and stable references. + * + * Components using this hook only re-render when the selected value + * actually changes, unlike useContext which re-renders on every context update. + * + * @param selector Function that extracts a value from the DataSource context. + * Does NOT need to be memoized by the caller. + */ +export function useDataSourceMasterDetailSingleValue( + selector: (ctx: DataSourceMasterDetailContextValue) => R, +): R { + const StoreContext = getDataSourceMasterDetailStoreContext(); + const store = useContext(StoreContext) as DataSourceMasterDetailStore; + return useComponentStoreSingleValue(store, selector); +} + +/** + * A selector hook that reads an object from the DataSource store. + * Uses shallow equality — re-renders only when a property of the + * returned object actually changes (by reference). + * + * Use this when the selector returns a derived object (e.g. picking + * multiple properties), so that a new object literal with the same + * values does not trigger a re-render. + * + * @param selector Function that extracts an object from the DataSource context. + * Does NOT need to be memoized by the caller. + */ +export function useDataSourceMasterDetailSelector( + selector: (ctx: DataSourceMasterDetailContextValue) => R, +): R | undefined { + const StoreContext = getDataSourceMasterDetailStoreContext(); + const store = useContext(StoreContext) as DataSourceMasterDetailStore; + + // Always call the hook to satisfy the Rules of Hooks. + // When no store exists, use a no-op store + no-op selector so + // useSyncExternalStore is still invoked unconditionally. + const result = useComponentStoreSelector( + store || NOOP_STORE, + store ? selector : (NOOP_SELECTOR as unknown as typeof selector), + ); + + return store ? result ?? undefined : undefined; +} + +export function useMasterRowInfo(): + | InfiniteTableRowInfo + | undefined { + const StoreContext = getDataSourceMasterDetailStoreContext(); + const store = useContext(StoreContext) as DataSourceMasterDetailStore; + + // Always call the hook to satisfy the Rules of Hooks. + const masterRowInfoSelector = store + ? (ctx: any) => ctx.masterRowInfo as InfiniteTableRowInfo + : (NOOP_SELECTOR as unknown as (ctx: any) => InfiniteTableRowInfo); + + const result = useComponentStoreSingleValue( + store || NOOP_STORE, + masterRowInfoSelector, + ); + + return store ? result : undefined; +} diff --git a/source/src/components/DataSource/publicHooks/useDataSourceSelector.ts b/source/src/components/DataSource/publicHooks/useDataSourceSelector.ts new file mode 100644 index 000000000..91555aecc --- /dev/null +++ b/source/src/components/DataSource/publicHooks/useDataSourceSelector.ts @@ -0,0 +1,66 @@ +import { useContext } from 'react'; + +import { getDataSourceStoreContext } from '../DataSourceContext'; + +import type { + DataSourceContextValue, + DataSourceStableContextValue, +} from '../types'; +import type { DataSourceStore } from '../DataSourceStore'; + +import { + useComponentStoreSelector, + useComponentStoreSingleValue, +} from '../../../utils/ComponentStore'; + +/** + * A selector hook that reads a single value from the DataSource store. + * Uses Object.is for equality — ideal for primitives and stable references. + * + * Components using this hook only re-render when the selected value + * actually changes, unlike useContext which re-renders on every context update. + * + * @param selector Function that extracts a value from the DataSource context. + * Does NOT need to be memoized by the caller. + */ +export function useDataSourceSingleValue( + selector: (ctx: DataSourceContextValue) => R, +): R { + const StoreContext = getDataSourceStoreContext(); + const store = useContext(StoreContext) as DataSourceStore; + return useComponentStoreSingleValue(store, selector); +} + +/** + * A selector hook that reads an object from the DataSource store. + * Uses shallow equality — re-renders only when a property of the + * returned object actually changes (by reference). + * + * Use this when the selector returns a derived object (e.g. picking + * multiple properties), so that a new object literal with the same + * values does not trigger a re-render. + * + * @param selector Function that extracts an object from the DataSource context. + * Does NOT need to be memoized by the caller. + */ +export function useDataSourceSelector( + selector: (ctx: DataSourceContextValue) => R, +): R { + const StoreContext = getDataSourceStoreContext(); + const store = useContext(StoreContext) as DataSourceStore; + return useComponentStoreSelector(store, selector); +} + +export function useDataSourceStableContext() { + return useDataSourceSelector((ctx) => { + const stableContext: DataSourceStableContextValue = { + getDataSourceState: ctx.getDataSourceState, + getDataSourceMasterContext: ctx.getDataSourceMasterContext, + dataSourceActions: ctx.dataSourceActions, + dataSourceApi: ctx.dataSourceApi, + assignState: ctx.assignState, + }; + + return stableContext; + }); +} diff --git a/source/src/components/DataSource/publicHooks/useDataSourceState.ts b/source/src/components/DataSource/publicHooks/useDataSourceState.ts index b88b3dfef..fc8ad55b3 100644 --- a/source/src/components/DataSource/publicHooks/useDataSourceState.ts +++ b/source/src/components/DataSource/publicHooks/useDataSourceState.ts @@ -3,14 +3,14 @@ import * as React from 'react'; import { DataSourceContextValue } from '../types'; import { getDataSourceContext } from '../DataSourceContext'; -import { DataSourceMasterDetailContextValue, DataSourceState } from '..'; -import { getDataSourceMasterDetailContext } from '../DataSourceMasterDetailContext'; +import { DataSourceState, useDataSourceSelector } from '..'; -export function useDataSourceState(): DataSourceState { - const DataSourceContext = getDataSourceContext(); - const contextValue = React.useContext(DataSourceContext); - - return contextValue.componentState; +export function useDataSourceState( + selector: (ctx: DataSourceState) => T, +): T { + return useDataSourceSelector((ctx) => { + return selector(ctx.dataSourceState); + }); } export function useDataSourceContextValue(): DataSourceContextValue { const DataSourceContext = getDataSourceContext(); @@ -18,12 +18,3 @@ export function useDataSourceContextValue(): DataSourceContextValue { return contextValue; } - -export function useMasterDetailContext(): - | DataSourceMasterDetailContextValue - | undefined { - const masterDetailContext = getDataSourceMasterDetailContext(); - const contextValue = React.useContext(masterDetailContext); - - return contextValue; -} diff --git a/source/src/components/DataSource/types.ts b/source/src/components/DataSource/types.ts index 7c901da8b..1f1e3c643 100644 --- a/source/src/components/DataSource/types.ts +++ b/source/src/components/DataSource/types.ts @@ -307,15 +307,15 @@ export interface DataSourceSetupState { debugTimings: Map; debugWarnings: Map; indexer: Indexer; - getDataSourceMasterContextRef: React.MutableRefObject< - () => DataSourceMasterDetailContextValue | undefined + getDataSourceMasterContextRef: React.RefObject< + () => DataSourceMasterDetailContextValue | undefined >; - __apiRef: React.MutableRefObject | null>; - lastSelectionUpdatedNodePathRef: React.MutableRefObject<{ + __apiRef: React.RefObject | null>; + lastSelectionUpdatedNodePathRef: React.RefObject<{ nodePath: NodePath; selected: boolean; } | null>; - lastExpandStateInfoRef: React.MutableRefObject<{ + lastExpandStateInfoRef: React.RefObject<{ state: 'collapsed' | 'expanded'; nodePath: NodePath | null; }>; @@ -1044,15 +1044,18 @@ export type DataSourceComponentActions = ComponentStateActions< DataSourceState >; -export interface DataSourceContextValue { - api: DataSourceApi; - getState: () => DataSourceState; +export interface DataSourceStableContextValue { + dataSourceApi: DataSourceApi; + getDataSourceState: () => DataSourceState; assignState: (state: Partial>) => void; getDataSourceMasterContext: () => | DataSourceMasterDetailContextValue | undefined; - componentState: DataSourceState; - componentActions: DataSourceComponentActions; + dataSourceActions: DataSourceComponentActions; +} +export interface DataSourceContextValue + extends DataSourceStableContextValue { + dataSourceState: DataSourceState; } export interface DataSourceMasterDetailContextValue { diff --git a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx index 8dffa0bd6..cfb0df5b3 100644 --- a/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx +++ b/source/src/components/HeadlessTable/ReactHeadlessTableRenderer.tsx @@ -1275,12 +1275,11 @@ export class GridRenderer extends Logger { } protected onMouseEnterNotBound = (event: React.MouseEvent) => { - const rowIndex = Number((event.target as HTMLElement).dataset.rowIndex); + const rowIndex = Number(event.currentTarget.dataset.rowIndex); this.onMouseEnter(rowIndex); }; protected onMouseLeaveNotBound = (event: React.MouseEvent) => { - const rowIndex = Number((event.target as HTMLElement).dataset.rowIndex); - + const rowIndex = Number(event.currentTarget.dataset.rowIndex); this.onMouseLeave(rowIndex); }; diff --git a/source/src/components/InfiniteTable/InfiniteTableContext.ts b/source/src/components/InfiniteTable/InfiniteTableContext.ts index 6fe917d7e..d7c7260e7 100644 --- a/source/src/components/InfiniteTable/InfiniteTableContext.ts +++ b/source/src/components/InfiniteTable/InfiniteTableContext.ts @@ -1,16 +1,17 @@ import { createContext } from 'react'; -import { InfiniteTableContextValue } from './types/InfiniteTableContextValue'; -let TableContext: any; +import type { InfiniteTableStore } from './InfiniteTableStore'; -export function getInfiniteTableContext(): React.Context< - InfiniteTableContextValue +let InfiniteTableStoreContext: any; + +export function getInfiniteTableStoreContext(): React.Context< + InfiniteTableStore > { - if (TableContext as React.Context>) { - return TableContext; + if (InfiniteTableStoreContext as React.Context>) { + return InfiniteTableStoreContext; } - return (TableContext = createContext>( - null as any as InfiniteTableContextValue, + return (InfiniteTableStoreContext = createContext>( + null as any as InfiniteTableStore, )); } diff --git a/source/src/components/InfiniteTable/InfiniteTableStore.ts b/source/src/components/InfiniteTable/InfiniteTableStore.ts new file mode 100644 index 000000000..b41d8bb8f --- /dev/null +++ b/source/src/components/InfiniteTable/InfiniteTableStore.ts @@ -0,0 +1,14 @@ +import { + ComponentStore, + createComponentStore, +} from '../../utils/ComponentStore'; +import type { InfiniteTableContextValue } from './types/InfiniteTableContextValue'; + +export interface InfiniteTableStore + extends ComponentStore> {} + +export function createInfiniteTableStore() { + return createComponentStore< + InfiniteTableContextValue + >() as InfiniteTableStore; +} diff --git a/source/src/components/InfiniteTable/components/FocusDetect.tsx b/source/src/components/InfiniteTable/components/FocusDetect.tsx index 17e21e43d..0984a2da4 100644 --- a/source/src/components/InfiniteTable/components/FocusDetect.tsx +++ b/source/src/components/InfiniteTable/components/FocusDetect.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { CSSProperties, useCallback } from 'react'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; -import { useInfiniteTable } from '../hooks/useInfiniteTable'; - import { focusLastFocusableCell } from '../utils/cellFocusUtils'; +import { useInfiniteTableStableContext } from '../hooks/useInfiniteTableSelector'; +import { useDataSourceStableContext } from '../../DataSource/publicHooks/useDataSourceSelector'; const style: CSSProperties = { width: 0, @@ -23,9 +22,9 @@ export function FocusDetect() { getComputed, dataSourceApi, dataSourceActions, - } = useInfiniteTable(); - const { getState: getDataSourceState, getDataSourceMasterContext } = - useDataSourceContextValue(); + } = useInfiniteTableStableContext(); + const { getDataSourceState, getDataSourceMasterContext } = + useDataSourceStableContext(); const { focusDetectDOMRef } = getState(); diff --git a/source/src/components/InfiniteTable/components/GroupingToolbar/index.tsx b/source/src/components/InfiniteTable/components/GroupingToolbar/index.tsx index c6e0ff8d2..b842c722c 100644 --- a/source/src/components/InfiniteTable/components/GroupingToolbar/index.tsx +++ b/source/src/components/InfiniteTable/components/GroupingToolbar/index.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import { useCallback } from 'react'; import { GroupingToolbarRecipe, GroupingToolbarItemRecipe } from './index.css'; -import { DataSourcePropGroupBy, useDataSourceState } from '../../../DataSource'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; +import { DataSourceApi, DataSourcePropGroupBy } from '../../../DataSource'; import { DragList } from '../draggable'; import { join } from '../../../../utils/join'; -import { InfiniteTableComputedColumn } from '../../types'; +import { + InfiniteTableApi, + InfiniteTableComputedColumn, + InfiniteTableComputedValues, +} from '../../types'; import { useDragDropProvider } from '../draggable/DragDropProvider'; import { DragListProps, useDragListContext } from '../draggable/DragList'; import { getColumnLabel } from '../InfiniteTableHeader/getColumnLabel'; @@ -19,6 +22,11 @@ import { } from './components'; import { SortIcon } from '../icons/SortIcon'; import { getGlobal } from '../../../../utils/getGlobal'; +import { + useInfiniteTableSelector, + useInfiniteTableStableContext, +} from '../../hooks/useInfiniteTableSelector'; +import { useDataSourceSelector } from '../../../DataSource/publicHooks/useDataSourceSelector'; export type GroupingToolbarProps = { orientation?: 'horizontal' | 'vertical'; @@ -91,7 +99,7 @@ export function GroupingToolbarItem(props: { const draggingInProgress = dragSourceListId === GROUPING_TOOLBAR_DRAG_LIST_ID; - const context = useInfiniteTable(); + const context = useInfiniteTableStableContext(); const columnHeader: React.ReactNode = (column ? getColumnLabel(column.id, context, 'grouping-toolbar') : field) ?? @@ -152,8 +160,20 @@ export function GroupingToolbarItem(props: { const GROUPING_TOOLBAR_DRAG_LIST_ID = 'grouping-toolbar'; export function GroupingToolbar(props: GroupingToolbarProps) { - const { groupBy } = useDataSourceState(); - const { getComputed, dataSourceApi, api } = useInfiniteTable(); + const { groupBy } = useDataSourceSelector((ctx) => { + return { + groupBy: ctx.dataSourceState.groupBy as DataSourcePropGroupBy, + }; + }); + const { getComputed, dataSourceApi, api } = useInfiniteTableSelector( + (ctx) => { + return { + getComputed: ctx.getComputed as () => InfiniteTableComputedValues, + dataSourceApi: ctx.dataSourceApi as DataSourceApi, + api: ctx.api as InfiniteTableApi, + }; + }, + ); const { dropTargetListId, dragSourceListId } = useDragDropProvider(); const { fieldsToColumn, computedColumnsMap } = getComputed(); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableBody/index.tsx b/source/src/components/InfiniteTable/components/InfiniteTableBody/index.tsx index 10d04baf3..2674696d7 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableBody/index.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableBody/index.tsx @@ -1,11 +1,6 @@ import * as React from 'react'; -import { - useDataSourceContextValue, - useMasterDetailContext, -} from '../../../DataSource/publicHooks/useDataSourceState'; import { HeadlessTable } from '../../../HeadlessTable'; import { useCellRendering } from '../../hooks/useCellRendering'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { useToggleWrapRowsHorizontally } from '../../hooks/useToggleWrapRowsHorizontally'; import { CellContextMenuLocationWithEvent, @@ -19,6 +14,12 @@ import { selectParentUntil } from '../../../../utils/selectParent'; import { getCellSelector } from '../../state/getInitialState'; import { LoadMask } from '../LoadMask'; import { useMemo } from 'react'; +import { + useInfiniteTableStableContext, + useInfiniteTableSelector, +} from '../../hooks/useInfiniteTableSelector'; +import { useDataSourceSelector } from '../../../DataSource'; +import { useMasterRowInfo } from '../../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; const _HOVERED_CLASS_NAMES = [ RowHoverCls, @@ -37,10 +38,10 @@ function toClassNameArray(str: string | string[]) { } function InfiniteTableBody(props: InfiniteTableBodyProps) { - const context = useInfiniteTable(); + const context = useInfiniteTableStableContext(); - const masterContext = useMasterDetailContext(); - const { state: componentState, getComputed, api } = context; + const masterRowInfo = useMasterRowInfo(); + const { getComputed, api } = context; const { renderer, onRenderUpdater, @@ -48,6 +49,7 @@ function InfiniteTableBody(props: InfiniteTableBodyProps) { keyboardNavigation, activeRowIndex, loadingText, + columnReorderDragColumnId, scrollStopDelay, brain, scrollerDOMRef, @@ -59,8 +61,34 @@ function InfiniteTableBody(props: InfiniteTableBodyProps) { wrapRowsHorizontally, domProps, domRef, + forceBodyRerenderTimestamp, rowHoverClassName, - } = componentState; + ready, + } = useInfiniteTableSelector((ctx) => { + return { + ready: ctx.state.ready, + columnReorderDragColumnId: ctx.state.columnReorderDragColumnId, + forceBodyRerenderTimestamp: ctx.state.forceBodyRerenderTimestamp, + renderer: ctx.state.renderer, + onRenderUpdater: ctx.state.onRenderUpdater, + debugId: ctx.state.debugId, + keyboardNavigation: ctx.state.keyboardNavigation, + activeRowIndex: ctx.state.activeRowIndex, + loadingText: ctx.state.loadingText, + scrollStopDelay: ctx.state.scrollStopDelay, + brain: ctx.state.brain, + scrollerDOMRef: ctx.state.scrollerDOMRef, + components: ctx.state.components, + bodySize: ctx.state.bodySize, + activeCellIndex: ctx.state.activeCellIndex, + rowDetailRenderer: ctx.state.rowDetailRenderer, + showHoverRows: ctx.state.showHoverRows, + wrapRowsHorizontally: ctx.state.wrapRowsHorizontally, + domProps: ctx.state.domProps, + domRef: ctx.state.domRef, + rowHoverClassName: ctx.state.rowHoverClassName, + }; + }); const LoadMaskCmp = components?.LoadMask ?? LoadMask; @@ -70,20 +98,22 @@ function InfiniteTableBody(props: InfiniteTableBodyProps) { const activeCellRowHeight = computedRowSizeCacheForDetails?.getRowHeight || computedRowHeight; - const { - componentState: { loading }, - } = useDataSourceContextValue(); + const { loading } = useDataSourceSelector((ctx) => { + return { + loading: ctx.dataSourceState.loading, + }; + }); const onContextMenu = React.useCallback((event: React.MouseEvent) => { const state = context.getState(); const target = event.target as HTMLElement; - if (!masterContext && (event as any)._from_row_detail) { + if (!masterRowInfo && (event as any)._from_row_detail) { // originating from detail grid. return; } - if (masterContext) { + if (masterRowInfo) { (event as any)._from_row_detail = true; } @@ -164,20 +194,18 @@ function InfiniteTableBody(props: InfiniteTableBodyProps) { return ( ) => e.stopPropagation(); export function InfiniteTableColumnHeaderFilter( props: InfiniteTableColumnHeaderFilterProps, ) { - const { filterOperatorMenuVisibleForColumnId } = useInfiniteTableState(); + const { filterOperatorMenuVisibleForColumnId } = useInfiniteTableSelector( + (ctx) => { + return { + filterOperatorMenuVisibleForColumnId: + ctx.state.filterOperatorMenuVisibleForColumnId, + }; + }, + ); const { column } = useInfiniteHeaderCell(); const FilterEditor = props.filterEditor; @@ -116,7 +125,7 @@ export function InfiniteTableColumnHeaderFilterEmpty() { } export function useInfiniteColumnFilterEditor() { - const context = useInfiniteTable(); + const context = useInfiniteTableStableContext(); const { column, columnApi } = useInfiniteHeaderCell(); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeader.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeader.tsx index 4dd4e7219..5eea883cc 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeader.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeader.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { join } from '../../../../utils/join'; import { RawTable } from '../../../HeadlessTable/RawTable'; @@ -9,7 +9,6 @@ import { TableRenderCellFnParam, } from '../../../HeadlessTable/rendererTypes'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { internalProps } from '../../internalProps'; import { InfiniteTableComputedColumnGroup } from '../../types/InfiniteTableProps'; import { CELL_DETACHED_CLASSNAMES } from '../cellDetachedCls'; @@ -20,6 +19,12 @@ import { InfiniteTableHeaderGroup } from './InfiniteTableHeaderGroup'; import type { InfiniteTableInternalHeaderProps } from './InfiniteTableHeaderTypes'; import type { ScrollPosition } from '../../../types/ScrollPosition'; import { DragList } from '../draggable'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; +import { InfiniteTableComputedColumn } from '../../types'; +import { + useDataSourceSelector, + useDataSourceStableContext, +} from '../../../DataSource/publicHooks/useDataSourceSelector'; const { rootClassName } = internalProps; @@ -39,6 +44,7 @@ function InfiniteTableInternalHeaderFn( ) { const { bodyBrain, + headerBrain, columns, style, className, @@ -49,18 +55,25 @@ function InfiniteTableInternalHeaderFn( } = props; const { - computed, getState, - state: { - headerBrain, - headerOptions, - showColumnFilters, - headerRenderer, - headerOnRenderUpdater, - }, - } = useInfiniteTable(); - - const { computedColumnsMap } = computed; + headerOptions, + headerRenderer, + headerOnRenderUpdater, + showColumnFilters, + computedColumnsMap, + } = useInfiniteTableSelector((ctx) => { + return { + computedColumnsMap: ctx.computed.computedColumnsMap as Map< + string, + InfiniteTableComputedColumn + >, + getState: ctx.getState, + headerOptions: ctx.state.headerOptions, + headerRenderer: ctx.state.headerRenderer, + headerOnRenderUpdater: ctx.state.headerOnRenderUpdater, + showColumnFilters: ctx.state.showColumnFilters, + }; + }); const domRef = useRef(null); const updateDOMTransform = useCallback((scrollPosition: ScrollPosition) => { @@ -91,6 +104,33 @@ function InfiniteTableInternalHeaderFn( style: { ...style, height: columnHeaderHeight }, }; + const { getDataSourceState, dataSourceApi, dataSourceActions } = + useDataSourceStableContext(); + + const { + allRowsSelected, + someRowsSelected, + selectionMode, + filterTypes, + filterDelay, + } = useDataSourceSelector((ctx) => { + return { + allRowsSelected: ctx.dataSourceState.allRowsSelected, + someRowsSelected: ctx.dataSourceState.someRowsSelected, + selectionMode: ctx.dataSourceState.selectionMode, + filterTypes: ctx.dataSourceState.filterTypes, + filterDelay: ctx.dataSourceState.filterDelay, + }; + }); + + const dataSourceStatePartialForHeaderCell = useMemo(() => { + return { + allRowsSelected, + someRowsSelected, + selectionMode, + }; + }, [allRowsSelected, someRowsSelected, selectionMode]); + const renderCell: TableRenderCellFn = useCallback( (params: TableRenderCellFnParam) => { const { @@ -148,6 +188,14 @@ function InfiniteTableInternalHeaderFn( column={column} horizontalLayoutPageIndex={horizontalLayoutPageIndex} headerOptions={headerOptions} + dataSourceStatePartialForHeaderCell={ + dataSourceStatePartialForHeaderCell + } + filterTypes={filterTypes} + filterDelay={filterDelay} + getDataSourceState={getDataSourceState} + dataSourceApi={dataSourceApi} + dataSourceActions={dataSourceActions} width={widthWithColspan} height={heightWithRowspan} columnsMap={computedColumnsMap} @@ -166,6 +214,12 @@ function InfiniteTableInternalHeaderFn( columnGroupsMaxDepth, showColumnFilters, headerBrain, + getDataSourceState, + dataSourceApi, + dataSourceActions, + filterTypes, + filterDelay, + dataSourceStatePartialForHeaderCell, ], ); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx index 7ce2b2394..6491d40f8 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderCell.tsx @@ -6,17 +6,13 @@ import { keyMirror } from '../../../../utils/keyMirror'; import { join } from '../../../../utils/join'; import { stripVar } from '../../../../utils/stripVar'; import { defaultFilterTypes } from '../../../DataSource/defaultFilterTypes'; -import { - useDataSourceState, - useDataSourceContextValue, -} from '../../../DataSource/publicHooks/useDataSourceState'; import { DataSourcePropFilterTypes } from '../../../DataSource/types'; import { setupResizeObserver } from '../../../ResizeObserver'; import { debounce } from '../../../utils/debounce'; import { getColumnApiForColumn } from '../../api/getColumnApi'; import { useCellClassName } from '../../hooks/useCellClassName'; import { useColumnPointerEvents } from '../../hooks/useColumnPointerEvents'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; import { InternalVars } from '../../internalVars.css'; import { ThemeVars } from '../../vars.css'; @@ -154,43 +150,65 @@ export const InfiniteHeaderCellDataAttributes = keyMirror({ [DRAG_ITEM_ATTRIBUTE]: '', }); -export function InfiniteTableHeaderCell( - props: InfiniteTableHeaderCellProps, -) { +function InfiniteTableHeaderCellFn(props: InfiniteTableHeaderCellProps) { const column: InfiniteTableComputedColumn = props.column; const { allowColumnHideWhileDragging: ALLOW_COLUMN_HIDE_ON_DRAG } = useInfiniteTableHeaderState(); + const { + dataSourceStatePartialForHeaderCell, + horizontalLayoutPageIndex, + + // DataSource context values passed as props + getDataSourceState, + dataSourceApi, + dataSourceActions, + + filterTypes, + filterDelay, + } = props; + + const { allRowsSelected, someRowsSelected, selectionMode } = + dataSourceStatePartialForHeaderCell; + const { onDragItemPointerDown } = useDragListContext(); + + // Use selector hooks instead of useInfiniteTable() to avoid + // re-rendering on every context change. Each selector only + // triggers a re-render when its specific value changes. + // Stable refs (same reference every render): + const { api, getComputed, getState, actions, - state: { - showColumnFilters, - components, - portalDOMRef, - columnHeaderHeight, - columnReorderDragColumnId, - columnMenuVisibleForColumnId, - columnReorderInPageIndex, - }, getDataSourceMasterContext, - } = useInfiniteTable(); - - const { - api: dataSourceApi, - componentActions: dataSourceActions, - getState: getDataSourceState, - componentState: { filterDelay, filterTypes }, - } = useDataSourceContextValue(); - - const { allRowsSelected, someRowsSelected, selectionMode } = - useDataSourceState(); + showColumnFilters, + components, + portalDOMRef, + columnHeaderHeight, + columnReorderDragColumnId, + columnMenuVisibleForColumnId, + columnReorderInPageIndex, + } = useInfiniteTableSelector((ctx) => { + return { + api: ctx.api, + getComputed: ctx.getComputed, + getState: ctx.getState, + actions: ctx.actions, + getDataSourceMasterContext: ctx.getDataSourceMasterContext, + showColumnFilters: ctx.state.showColumnFilters, + components: ctx.state.components, + portalDOMRef: ctx.state.portalDOMRef, + columnHeaderHeight: ctx.state.columnHeaderHeight, + columnReorderDragColumnId: ctx.state.columnReorderDragColumnId, + columnMenuVisibleForColumnId: ctx.state.columnMenuVisibleForColumnId, + columnReorderInPageIndex: ctx.state.columnReorderInPageIndex, + }; + }); - const horizontalLayoutPageIndex = props.horizontalLayoutPageIndex; const dragging = columnReorderDragColumnId === column.id; const insideDisabledDraggingPage = @@ -224,7 +242,7 @@ export function InfiniteTableHeaderCell( ? column.verticalAlign({ isHeader: true, column }) : column.verticalAlign) ?? 'center'; - const columnApi = getColumnApiForColumn(column, { + const columnApi = getColumnApiForColumn(column, { actions, api, dataSourceActions, @@ -723,6 +741,10 @@ export function InfiniteTableHeaderCell( ); } +export const InfiniteTableHeaderCell = React.memo( + InfiniteTableHeaderCellFn, +) as typeof InfiniteTableHeaderCellFn; + export function useInfiniteHeaderCell() { const result = useContext( InfiniteTableHeaderCellContext, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderWrapper.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderWrapper.tsx index 0be4d5e3e..b695f2e7d 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderWrapper.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/InfiniteTableHeaderWrapper.tsx @@ -4,12 +4,12 @@ import type { Scrollbars } from '../..'; import { useMatrixBrain } from '../../../HeadlessTable'; import { getScrollbarWidth } from '../../../utils/getScrollbarWidth'; import { MatrixBrain } from '../../../VirtualBrain/MatrixBrain'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { buildColumnAndGroupTree } from './buildColumnAndGroupTree'; import { HeaderScrollbarPlaceholderCls, HeaderWrapperCls } from './header.css'; import { InfiniteTableHeaderWrapperClassName } from './headerClassName'; import { InfiniteTableInternalHeader } from './InfiniteTableHeader'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; export type TableHeaderWrapperProps = { headerBrain: MatrixBrain; @@ -17,27 +17,38 @@ export type TableHeaderWrapperProps = { scrollbars: Scrollbars; wrapRowsHorizontally: boolean; }; -export function TableHeaderWrapper(props: TableHeaderWrapperProps) { +export function TableHeaderWrapper<_T>(props: TableHeaderWrapperProps) { const { headerBrain, bodyBrain, scrollbars } = props; - const tableContextValue = useInfiniteTable(); - const { computedPinnedStartColumns, computedPinnedEndColumns, computedVisibleColumns, columnSize, - } = tableContextValue.computed; + } = useInfiniteTableSelector((ctx) => { + return { + computedPinnedStartColumns: ctx.computed.computedPinnedStartColumns, + computedPinnedEndColumns: ctx.computed.computedPinnedEndColumns, + computedVisibleColumns: ctx.computed.computedVisibleColumns, + columnSize: ctx.computed.columnSize, + }; + }); const { - state: { - columnHeaderHeight, - columnGroupsDepthsMap, - columnGroupsMaxDepth, - computedColumnGroups, - showColumnFilters, - }, - } = tableContextValue; + columnHeaderHeight, + columnGroupsDepthsMap, + columnGroupsMaxDepth, + computedColumnGroups, + showColumnFilters, + } = useInfiniteTableSelector((ctx) => { + return { + columnHeaderHeight: ctx.state.columnHeaderHeight, + columnGroupsDepthsMap: ctx.state.columnGroupsDepthsMap, + columnGroupsMaxDepth: ctx.state.columnGroupsMaxDepth, + computedColumnGroups: ctx.state.computedColumnGroups, + showColumnFilters: ctx.state.showColumnFilters, + }; + }); const rows = !computedColumnGroups || !Object.keys(computedColumnGroups).length diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/ResizeHandle/index.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/ResizeHandle/index.tsx index 84625b587..85b2ae8cd 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/ResizeHandle/index.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/ResizeHandle/index.tsx @@ -11,7 +11,7 @@ import { ResizeHandleDraggerClsRecipe, ResizeHandleRecipeCls, } from './ResizeHandle.css'; -import { useInfiniteTable } from '../../../hooks/useInfiniteTable'; +import { useInfiniteTableSelector } from '../../../hooks/useInfiniteTableSelector'; type ResizeHandleProps = { horizontalLayoutPageIndex: number | null; @@ -44,9 +44,12 @@ const { rootClassName } = internalProps; export const InfiniteTableHeaderCellResizeHandleCls = `${rootClassName}HeaderCell_ResizeHandle`; function ResizeHandleFn(props: ResizeHandleProps) { - const { - state: { brain, headerBrain }, - } = useInfiniteTable(); + const { brain, headerBrain } = useInfiniteTableSelector((ctx) => { + return { + brain: ctx.state.brain, + headerBrain: ctx.state.headerBrain, + }; + }); const domRef = useRef(null); const [constrained, setConstrained] = useState(false); const constrainedRef = useRef(constrained); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnGroupResizeHandle.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnGroupResizeHandle.tsx index f6cdfdc1b..0cee551d9 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnGroupResizeHandle.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnGroupResizeHandle.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { useCallback } from 'react'; import { computeGroupResize } from '../../../flexbox'; import { MatrixBrain } from '../../../VirtualBrain/MatrixBrain'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { InfiniteTableComputedColumn, InfiniteTablePropColumnSizing, @@ -10,6 +9,7 @@ import { import { InfiniteTableComputedColumnGroup } from '../../types/InfiniteTableProps'; import { GroupResizeHandle } from './ResizeHandle/GroupResizeHandle'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; export function useColumnGroupResizeHandle( groupColumns: InfiniteTableComputedColumn[], @@ -21,12 +21,16 @@ export function useColumnGroupResizeHandle( }, ) { const { bodyBrain, groupHeight, columnGroup, columnGroupsMaxDepth } = config; - const { - computed: { computedVisibleColumns }, - getState, - getComputed, - actions, - } = useInfiniteTable(); + const { computedVisibleColumns, getState, getComputed, actions } = + useInfiniteTableSelector((ctx) => { + return { + computedVisibleColumns: ctx.computed + .computedVisibleColumns as InfiniteTableComputedColumn[], + getState: ctx.getState, + getComputed: ctx.getComputed, + actions: ctx.actions, + }; + }); const lastColumnInGroup = groupColumns[groupColumns.length - 1]; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnResizeHandle.tsx b/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnResizeHandle.tsx index 0d98fa368..a54fa60fe 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnResizeHandle.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableHeader/useColumnResizeHandle.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useCallback } from 'react'; import { computeResize } from '../../../flexbox'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; import { InfiniteTableComputedColumn, InfiniteTablePropColumnSizing, @@ -14,12 +14,15 @@ export function useColumnResizeHandle( horizontalLayoutPageIndex: number | null; }, ) { - const { - computed: { computedVisibleColumns }, - getState, - getComputed, - actions, - } = useInfiniteTable(); + const { computedVisibleColumns, getState, getComputed, actions } = + useInfiniteTableSelector((ctx) => { + return { + computedVisibleColumns: ctx.computed.computedVisibleColumns, + getState: ctx.getState, + getComputed: ctx.getComputed, + actions: ctx.actions, + }; + }); const computeResizeForDiff = useCallback( ({ diff, @@ -112,7 +115,7 @@ export function useColumnResizeHandle( return result; }, - [column], + [column, getState, getComputed], ); const onColumnResize = useCallback( @@ -133,7 +136,7 @@ export function useColumnResizeHandle( } actions.columnSizing = columnSizing; }, - [computeResizeForDiff], + [computeResizeForDiff, actions], ); const resizeHandle = column.computedResizable ? ( diff --git a/source/src/components/InfiniteTable/components/InfiniteTablePublicHeader/index.tsx b/source/src/components/InfiniteTable/components/InfiniteTablePublicHeader/index.tsx index 100d9d72c..25e87d4f3 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTablePublicHeader/index.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTablePublicHeader/index.tsx @@ -6,8 +6,8 @@ import { } from '../../state/getInfiniteHeaderState'; import { InfiniteTableHeaderProps } from './types'; import { getInfiniteTableHeaderContext } from './context'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { TableHeaderWrapper } from '../InfiniteTableHeader/InfiniteTableHeaderWrapper'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; const { ManagedComponentContextProvider: InfiniteTableHeaderRoot } = buildManagedComponent({ @@ -18,10 +18,17 @@ const { ManagedComponentContextProvider: InfiniteTableHeaderRoot } = }); export function InfiniteTableHeader(props: InfiniteTableHeaderProps) { - const context = useInfiniteTable(); + const { getComputed, header, brain, headerBrain, wrapRowsHorizontally } = + useInfiniteTableSelector((ctx) => { + return { + getComputed: ctx.getComputed, + header: ctx.state.header, + brain: ctx.state.brain, + headerBrain: ctx.state.headerBrain, + wrapRowsHorizontally: ctx.state.wrapRowsHorizontally, + }; + }); - const { state: componentState, getComputed } = context; - const { header, brain, headerBrain, wrapRowsHorizontally } = componentState; const { scrollbars } = getComputed(); return header ? ( diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx index f4ea1f43f..a3e3f9c3f 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/FlashingColumnCell.tsx @@ -4,9 +4,9 @@ import { useEffectWhen } from '../../../hooks/useEffectWhen'; import { useInfiniteColumnCell } from './InfiniteTableColumnCell'; import { join } from '../../../../utils/join'; import { FlashingColumnCellRecipe } from '../cell.css'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { InternalVars } from '../../internalVars.css'; import { stripVar } from '../../../../utils/stripVar'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; export type FlashingCellOptions = { flashDuration?: number; @@ -61,9 +61,13 @@ export const createFlashingColumnCellComponent = ( ) => { const cellContext = useInfiniteColumnCell(); - const { - state: { flashingDurationCSSVarValue }, - } = useInfiniteTable(); + const { flashingDurationCSSVarValue } = useInfiniteTableSelector( + (ctx) => { + return { + flashingDurationCSSVarValue: ctx.state.flashingDurationCSSVarValue, + }; + }, + ); const duration = typeof flashingDurationCSSVarValue === 'number' diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts index d2caf2a58..6bbf34993 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableCellTypes.ts @@ -124,6 +124,17 @@ export interface InfiniteTableHeaderCellProps InfiniteTableCellProps, 'children' | 'cellType' | 'renderChildren' > { + // DataSource context values passed as props to avoid context re-renders + getDataSourceState: () => DataSourceState; + dataSourceApi: DataSourceApi; + dataSourceActions: DataSourceComponentActions; + dataSourceStatePartialForHeaderCell: { + allRowsSelected: DataSourceState['allRowsSelected']; + someRowsSelected: DataSourceState['someRowsSelected']; + selectionMode: DataSourceState['selectionMode']; + }; + filterTypes: DataSourceState['filterTypes']; + filterDelay: DataSourceState['filterDelay']; columnsMap: Map>; height: number; headerOptions: InfiniteTablePropHeaderOptions; diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx index b7a3adecd..9290c9387 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableColumnCell.tsx @@ -11,7 +11,6 @@ import { join } from '../../../../utils/join'; import { stripVar } from '../../../../utils/stripVar'; import { useCellClassName } from '../../hooks/useCellClassName'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; import { InternalVars } from '../../internalVars.css'; import { InfiniteColumnEditorContextType, @@ -56,6 +55,7 @@ import { InfiniteTableColumnEditor } from './InfiniteTableColumnEditor'; import { TreeColumnCellExpanderCls } from './row.css'; import { InfiniteTableColumnCellClassName } from './InfiniteTableColumnCellClassNames'; import { objectValuesExcept } from '../../utils/objectValuesExcept'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; const columnZIndexAtIndex = stripVar(InternalVars.columnZIndexAtIndex); const columnVisibilityAtIndex = stripVar(InternalVars.columnVisibilityAtIndex); @@ -929,10 +929,15 @@ export function useInfiniteColumnCell() { export function useInfiniteColumnEditor< T, >(): InfiniteColumnEditorContextType { - const { - api, - state: { editingValueRef, editingCell }, - } = useInfiniteTable(); + const { api, editingValueRef, editingCell } = useInfiniteTableSelector( + (ctx) => { + return { + api: ctx.api, + editingCell: ctx.state.editingCell, + editingValueRef: ctx.state.editingValueRef, + }; + }, + ); const { column, rowInfo } = useInfiniteColumnCell(); diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableDetailRow.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableDetailRow.tsx index 7bbeaa2fa..37e589ba8 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableDetailRow.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/InfiniteTableDetailRow.tsx @@ -7,10 +7,11 @@ import { InternalVars } from '../../internalVars.css'; import { InfiniteTableProps, InfiniteTableRowInfo } from '../../types'; import { RowDetailRecipe } from '../rowDetail.css'; -import { getDataSourceMasterDetailContext } from '../../../../components/DataSource/DataSourceMasterDetailContext'; +import { getDataSourceMasterDetailStoreContext } from '../../../../components/DataSource/DataSourceMasterDetailContext'; import { NonUndefined } from '../../../types/NonUndefined'; import { useRegisterDetail } from './useRegisterDetail'; +import { createDataSourceMasterDetailStore } from '../../../DataSource/DataSourceMasterDetailStore'; type InfiniteTableDetailRowProps = { rowInfo: InfiniteTableRowInfo; @@ -27,7 +28,6 @@ const { rootClassName } = internalProps; export const InfiniteTableRowDetailsClassName = `${rootClassName}RowDetail`; function InfiniteTableDetailRowFn(props: InfiniteTableDetailRowProps) { - const DataSourceMasterDetailContext = getDataSourceMasterDetailContext(); const { domRef, rowDetailRenderer, @@ -42,8 +42,24 @@ function InfiniteTableDetailRowFn(props: InfiniteTableDetailRowProps) { rowInfo, }); + const DataSourceMasterDetailStoreContext = + getDataSourceMasterDetailStoreContext(); + + const [masterDetailStore] = React.useState(() => + createDataSourceMasterDetailStore(), + ); + + masterDetailStore.setSnapshot(masterDetailContextValue); + + React.useLayoutEffect(() => { + masterDetailStore.notify(); + // this value is different whenever masterRowInfo.id changes + // so we're fine with this not triggering a notification + // on all renders + }, [masterDetailContextValue]); + return ( - +
(props: InfiniteTableDetailRowProps) { > {rowDetailRenderer(rowInfo, currentRowCache)}
-
+ ); } diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/columnRendering.tsx b/source/src/components/InfiniteTable/components/InfiniteTableRow/columnRendering.tsx index b3066c5d7..c7af22ab9 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/columnRendering.tsx +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/columnRendering.tsx @@ -129,6 +129,7 @@ export function getCellContext( isParentNode: false, isGroupRow: true, isTreeNode: false, + nodeExpanded: false, rowDetailState: false, data: rowInfo.data, rowActive, @@ -181,6 +182,7 @@ export function getCellContext( isTreeNode: false, isParentNode: false, isGroupRow: false, + nodeExpanded: false, rowDetailState: rowDetailState, data: rowInfo.data, rowActive, @@ -551,6 +553,7 @@ export function getFormattedValueParamForCell( isGroupRow: true, isTreeNode: false, isParentNode: false, + nodeExpanded: false, data: rowInfo.data, rowDetailState: false as false | 'expanded' | 'collapsed', rowSelected, @@ -594,6 +597,7 @@ export function getFormattedValueParamForCell( isGroupRow: false, isTreeNode: false, isParentNode: false, + nodeExpanded: false, data: rowInfo.data, rowDetailState, rowSelected, diff --git a/source/src/components/InfiniteTable/components/InfiniteTableRow/useRegisterDetail.ts b/source/src/components/InfiniteTable/components/InfiniteTableRow/useRegisterDetail.ts index f265b6651..da946e781 100644 --- a/source/src/components/InfiniteTable/components/InfiniteTableRow/useRegisterDetail.ts +++ b/source/src/components/InfiniteTable/components/InfiniteTableRow/useRegisterDetail.ts @@ -4,8 +4,8 @@ import { DataSourceMasterDetailContextValue, DataSourceState, RowDetailCache, + useDataSourceSelector, } from '../../../../components/DataSource'; -import { useDataSourceContextValue } from '../../../../components/DataSource/publicHooks/useDataSourceState'; import { useMemo, useRef } from 'react'; import { @@ -15,7 +15,7 @@ import { } from '../../../../components/DataSource/state/getInitialState'; import { InfiniteTableRowInfo } from '../../types'; import { once } from '../../../../utils/DeepMap/once'; -import { useInfiniteTable } from '../../hooks/useInfiniteTable'; +import { useInfiniteTableSelector } from '../../hooks/useInfiniteTableSelector'; type UseRegisterDetailProps = { rowDetailsCache: RowDetailCache; @@ -107,11 +107,20 @@ function useCurrentRowCache( export function useRegisterDetail(props: UseRegisterDetailProps) { const { rowDetailsCache, rowInfo } = props; const { - getState: getMasterDataSourceState, - componentActions: masterActions, - } = useDataSourceContextValue(); + getDataSourceState: getMasterDataSourceState, + dataSourceActions: masterActions, + } = useDataSourceSelector((ctx) => { + return { + getDataSourceState: ctx.getDataSourceState as () => DataSourceState, + dataSourceActions: ctx.dataSourceActions, + }; + }); - const { getState: getMasterState } = useInfiniteTable(); + const { getMasterState } = useInfiniteTableSelector((ctx) => { + return { + getMasterState: ctx.getState, + }; + }); const { currentRowCache, cacheCalledByRowDetailRenderer } = useCurrentRowCache(rowInfo.id, rowDetailsCache); @@ -139,10 +148,10 @@ export function useRegisterDetail(props: UseRegisterDetailProps) { // to be set - if we won't, they can trigger a data load, // which we want to avoid requestAnimationFrame(() => { - if (detailContext.componentState.destroyedRef.current) { + if (detailContext.dataSourceState.destroyedRef.current) { return; } - detailContext.componentActions.stateReadyAsDetails = true; + detailContext.dataSourceActions.stateReadyAsDetails = true; }); return; } @@ -157,13 +166,13 @@ export function useRegisterDetail(props: UseRegisterDetailProps) { // to be set - if we won't, they can trigger a data load, // which we want to avoid requestAnimationFrame(() => { - if (detailContext.componentState.destroyedRef.current) { + if (detailContext.dataSourceState.destroyedRef.current) { return; } - detailContext.componentActions.stateReadyAsDetails = true; + detailContext.dataSourceActions.stateReadyAsDetails = true; }); - detailContext.componentState.onCleanup.onChange(() => { + detailContext.dataSourceState.onCleanup.onChange(() => { const cacheEntryForRow = currentRowCache.get(); if (!cacheEntryForRow) { @@ -173,7 +182,7 @@ export function useRegisterDetail(props: UseRegisterDetailProps) { } updateDetailStateToRestoreForRowId(rowInfo.id, { - detailState: detailContext.getState(), + detailState: detailContext.getDataSourceState(), masterActions, masterState: getMasterDataSourceState(), cacheEntryForRow, diff --git a/source/src/components/InfiniteTable/components/draggable/DragList.tsx b/source/src/components/InfiniteTable/components/draggable/DragList.tsx index 3dfd98812..f5af82fa3 100644 --- a/source/src/components/InfiniteTable/components/draggable/DragList.tsx +++ b/source/src/components/InfiniteTable/components/draggable/DragList.tsx @@ -519,10 +519,6 @@ export const DragList = (props: DragListProps) => { ], ); - const isDragging = (id: string | number | undefined) => { - return id === undefined ? dragItemId != null : `${id}` === dragItemId; - }; - const dragging = !!dragItemId; const contextValue = React.useMemo(() => { @@ -540,7 +536,6 @@ export const DragList = (props: DragListProps) => { }, [ dragItemId, onDragItemPointerDown, - isDragging, dragging, dragSourceListId, dropTargetListId, diff --git a/source/src/components/InfiniteTable/eventHandlers/index.ts b/source/src/components/InfiniteTable/eventHandlers/index.ts index 3796696b5..d6b1fc395 100644 --- a/source/src/components/InfiniteTable/eventHandlers/index.ts +++ b/source/src/components/InfiniteTable/eventHandlers/index.ts @@ -1,29 +1,26 @@ import type { KeyboardEvent, MouseEvent } from 'react'; import { useCallback, useMemo, useEffect } from 'react'; import { cloneTreeSelection } from '../../DataSource/TreeApi'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; import { CellPositionByIndex } from '../../types/CellPositionByIndex'; import { getColumnApiForColumn } from '../api/getColumnApi'; import { cloneRowSelection } from '../api/getRowSelectionApi'; -import { useInfiniteTable } from '../hooks/useInfiniteTable'; import { InfiniteTableEventHandlerContext } from './eventHandlerTypes'; import { onCellClick } from './onCellClick'; import { onCellMouseDown } from './onCellMouseDown'; import { onKeyDown } from './onKeyDown'; +import { + useInfiniteTableSelector, + useInfiniteTableStableContext, +} from '../hooks/useInfiniteTableSelector'; +import { InfiniteTableState } from '../types'; +import { useDataSourceStableContext } from '../../DataSource/publicHooks/useDataSourceSelector'; function useEventHandlersContext() { - const { - getState, - actions: actions, - api, - getComputed, - getDataSourceMasterContext, - } = useInfiniteTable(); - const { - getState: getDataSourceState, - componentActions: dataSourceActions, - api: dataSourceApi, - } = useDataSourceContextValue(); + const { getState, actions, api, getComputed, getDataSourceMasterContext } = + useInfiniteTableStableContext(); + + const { getDataSourceState, dataSourceActions, dataSourceApi } = + useDataSourceStableContext(); const context = useMemo(() => { const context: InfiniteTableEventHandlerContext = { @@ -124,7 +121,11 @@ function handleDOMEvents() { } function subscribeToDOMEvents() { - const { getState } = useInfiniteTable(); + const { getState } = useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState as () => InfiniteTableState, + }; + }); const onKeyDown = useCallback((event: KeyboardEvent) => { getState().keyDown(event); diff --git a/source/src/components/InfiniteTable/hooks/useCellRendering.tsx b/source/src/components/InfiniteTable/hooks/useCellRendering.tsx index 05d144d6e..a717b36fb 100644 --- a/source/src/components/InfiniteTable/hooks/useCellRendering.tsx +++ b/source/src/components/InfiniteTable/hooks/useCellRendering.tsx @@ -2,7 +2,6 @@ import { Ref, useMemo } from 'react'; import { useCallback, useEffect, useRef } from 'react'; import * as React from 'react'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; import { TableRenderCellFn, TableRenderCellFnParam, @@ -10,16 +9,20 @@ import { TableRenderDetailRowFnParam, } from '../../HeadlessTable/rendererTypes'; import { useLatest } from '../../hooks/useLatest'; -import { useRerender } from '../../hooks/useRerender'; import type { Size } from '../../types/Size'; import { InfiniteTableColumnCellProps } from '../components/InfiniteTableRow/InfiniteTableCellTypes'; import { InfiniteTableColumnCell } from '../components/InfiniteTableRow/InfiniteTableColumnCell'; -import type { InfiniteTableComputedValues, InfiniteTableApi } from '../types'; +import type { + InfiniteTableComputedValues, + InfiniteTableApi, + InfiniteTableProps, +} from '../types'; -import { useInfiniteTable } from './useInfiniteTable'; import { useYourBrain } from './useYourBrain'; import { InfiniteTableDetailRow } from '../components/InfiniteTableRow/InfiniteTableDetailRow'; import { visibility } from '../utilities.css'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; +import { DataSourceState, useDataSourceSelector } from '../../DataSource'; type CellRenderingParam = { computed: InfiniteTableComputedValues; @@ -44,8 +47,72 @@ export function useCellRendering( ): CellRenderingResult { const { computed, bodySize, imperativeApi } = param; - const { actions, state, getState, getComputed, getDataSourceMasterContext } = - useInfiniteTable(); + const { + actions, + getState, + getComputed, + getDataSourceMasterContext, + rowHeight, + rowDetailHeight, + onRowMouseEnter, + onRowMouseLeave, + groupRenderStrategy, + brain, + showZebraRows, + rowDetailRenderer, + isRowDetailsExpanded, + isRowDetailsEnabled, + cellClassName, + cellStyle, + rowStyle, + rowDetailCache, + rowClassName, + onScrollToTop, + onScrollToBottom, + onScrollStop, + scrollToBottomOffset, + wrapRowsHorizontally, + updatedAt, + ready, + editingCell, + } = useInfiniteTableSelector((ctx) => { + return { + actions: ctx.actions, + getState: ctx.getState, + getComputed: ctx.getComputed, + getDataSourceMasterContext: ctx.getDataSourceMasterContext, + rowHeight: ctx.state.rowHeight, + rowDetailHeight: ctx.state.rowDetailHeight, + + onRowMouseEnter: ctx.state + .onRowMouseEnter as InfiniteTableProps['onRowMouseEnter'], + onRowMouseLeave: ctx.state + .onRowMouseLeave as InfiniteTableProps['onRowMouseLeave'], + + groupRenderStrategy: ctx.state.groupRenderStrategy, + brain: ctx.state.brain, + showZebraRows: ctx.state.showZebraRows, + rowDetailRenderer: ctx.state + .rowDetailRenderer as InfiniteTableProps['rowDetailRenderer'], + isRowDetailsExpanded: ctx.state + .isRowDetailExpanded as InfiniteTableProps['isRowDetailExpanded'], + isRowDetailsEnabled: ctx.state + .isRowDetailEnabled as InfiniteTableProps['isRowDetailEnabled'], + cellClassName: ctx.state.cellClassName, + cellStyle: ctx.state.cellStyle, + rowStyle: ctx.state.rowStyle, + rowDetailCache: ctx.state.rowDetailCache, + rowClassName: ctx.state.rowClassName, + onScrollToTop: ctx.state.onScrollToTop, + onScrollToBottom: ctx.state.onScrollToBottom, + onScrollStop: ctx.state.onScrollStop, + scrollToBottomOffset: ctx.state.scrollToBottomOffset, + wrapRowsHorizontally: ctx.state.wrapRowsHorizontally, + updatedAt: ctx.state.updatedAt, + ready: ctx.state.ready, + editingCell: ctx.state.editingCell, + }; + }); const { computedPinnedStartColumns, @@ -60,51 +127,30 @@ export function useCellRendering( columnSize, } = computed; - const { - componentState: dataSourceState, - getState: getDataSourceState, - componentActions: dataSourceActions, - api: dataSourceApi, - } = useDataSourceContextValue(); - const { dataArray, rowInfoStore, selectionMode, cellSelection, isNodeReadOnly, - } = dataSourceState; + getDataSourceState, + dataSourceApi, + dataSourceActions, + } = useDataSourceSelector((ctx) => { + return { + dataSourceApi: ctx.dataSourceApi, + dataSourceActions: ctx.dataSourceActions, + getDataSourceState: ctx.getDataSourceState, + dataArray: ctx.dataSourceState + .dataArray as DataSourceState['dataArray'], + rowInfoStore: ctx.dataSourceState.rowInfoStore, + selectionMode: ctx.dataSourceState.selectionMode, + cellSelection: ctx.dataSourceState.cellSelection, + isNodeReadOnly: ctx.dataSourceState.isNodeReadOnly, + }; + }); const getData = useLatest(dataArray); - const { - rowHeight, - rowDetailHeight, - - onRowMouseEnter, - onRowMouseLeave, - - groupRenderStrategy, - brain, - showZebraRows, - rowDetailRenderer, - isRowDetailExpanded: isRowDetailsExpanded, - isRowDetailEnabled: isRowDetailsEnabled, - cellClassName, - cellStyle, - rowStyle, - rowDetailCache: rowDetailsCache, - rowClassName, - onScrollToTop, - onScrollToBottom, - onScrollStop, - scrollToBottomOffset, - wrapRowsHorizontally, - updatedAt: componentStateUpdatedAt, - ready, - editingCell, - } = state; - - const repaintId = dataSourceState.updatedAt; useYourBrain({ columnSize, @@ -195,11 +241,11 @@ export function useCellRendering( } }, [ready]); - const [, rerender] = useRerender(); + // const [, rerender] = useRerender(); - useEffect(() => { - rerender(); // TODO check this is still needed - }, [dataSourceState]); + // useEffect(() => { + // rerender(); // TODO check this is still needed + // }, [dataSourceState]); const dataSourceStatePartialForCell = useMemo(() => { return { @@ -337,7 +383,6 @@ export function useCellRendering( toggleGroupRow, showZebraRows, brain, - repaintId, rowInfoStore, rowStyle, rowClassName, @@ -354,7 +399,7 @@ export function useCellRendering( getComputed, getDataSourceMasterContext, - componentStateUpdatedAt, + updatedAt, editingCell, dataSourceStatePartialForCell, ], @@ -389,7 +434,7 @@ export function useCellRendering( return ( rowInfo={rowInfo} - rowDetailsCache={rowDetailsCache} + rowDetailsCache={rowDetailCache} rowIndex={rowIndex} domRef={domRef} detailOffset={rowHeight} @@ -404,12 +449,11 @@ export function useCellRendering( rowDetailRenderer, rowDetailHeight, isRowDetailsExpanded, - rowDetailsCache, + rowDetailCache, ]); return { renderCell, renderDetailRow, - // repaintId, }; } diff --git a/source/src/components/InfiniteTable/hooks/useColumnFilterOperatorMenu.ts b/source/src/components/InfiniteTable/hooks/useColumnFilterOperatorMenu.ts index 4e8256b2a..9722db876 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnFilterOperatorMenu.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnFilterOperatorMenu.ts @@ -1,29 +1,31 @@ import { useEffect } from 'react'; import { Rectangle } from '../../../utils/pageGeometry/Rectangle'; -import { useMasterDetailContext } from '../../DataSource/publicHooks/useDataSourceState'; import { useOverlay } from '../../hooks/useOverlay'; import { getFilterOperatorMenuForColumn } from '../utils/getFilterOperatorMenuForColumn'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableStableContext } from './useInfiniteTableSelector'; +import { useDataSourceMasterDetailSelector } from '../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; const OFFSET = 10; export function useColumnFilterOperatorMenu() { - const context = useInfiniteTable(); - const masterContext = useMasterDetailContext(); - const { getState, actions } = context; + const { portalDOMRef } = + useDataSourceMasterDetailSelector((ctx) => { + return { + portalDOMRef: ctx.getMasterState().portalDOMRef, + }; + }) || {}; + const stableContext = useInfiniteTableStableContext(); + const { getState, actions } = stableContext; const { showOverlay, portal: menuPortal, clearAll, } = useOverlay({ - portalContainer: masterContext - ? masterContext.getMasterState().portalDOMRef.current - : false, + portalContainer: portalDOMRef?.current ?? false, }); useEffect(() => { - const { actions: actions, getState } = context; const state = getState(); return state.onFilterOperatorMenuClick.onChange((info) => { @@ -50,7 +52,12 @@ export function useColumnFilterOperatorMenu() { rect.height += 2 * OFFSET; showOverlay( - () => getFilterOperatorMenuForColumn(column.id, context, onHideIntent), + () => + getFilterOperatorMenuForColumn( + column.id, + stableContext, + onHideIntent, + ), { constrainTo: getState().domRef.current!, id: 'filter-operator-menu', diff --git a/source/src/components/InfiniteTable/hooks/useColumnMenu.ts b/source/src/components/InfiniteTable/hooks/useColumnMenu.ts index 42210e874..5c89dc06c 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnMenu.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnMenu.ts @@ -1,21 +1,26 @@ import { useEffect } from 'react'; -import { useMasterDetailContext } from '../../DataSource/publicHooks/useDataSourceState'; + import { ShowOverlayFn, useOverlay } from '../../hooks/useOverlay'; import { MenuIconDataAttributes, MenuIconDataAttributesValues, } from '../components/icons/MenuIcon'; import { InfiniteHeaderCellDataAttributes } from '../components/InfiniteTableHeader/InfiniteTableHeaderCell'; -import { InfiniteTableContextValue } from '../types'; import { getMenuForColumn } from '../utils/getMenuForColumn'; -import { useInfiniteTable } from './useInfiniteTable'; + +import { + useInfiniteTableSelector, + useInfiniteTableStableContext, +} from './useInfiniteTableSelector'; +import { InfiniteTableStableContextValue } from '../types/InfiniteTableContextValue'; +import { useDataSourceMasterDetailSelector } from '../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; const menuIconSelector = `[${MenuIconDataAttributes['data-name']}="${MenuIconDataAttributesValues['data-name']}"]`; function showMenuForColumn(options: { columnId: string; target?: HTMLElement; - context: InfiniteTableContextValue; + context: InfiniteTableStableContextValue; clearAll: VoidFunction; showOverlay: ShowOverlayFn; }) { @@ -67,22 +72,37 @@ function showMenuForColumn(options: { } export function useColumnMenu() { - const context = useInfiniteTable(); + const context = useInfiniteTableStableContext(); + const { + getState, + actions, + columnMenuVisibleForColumnId, + columnMenuVisibleKey, + } = useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState, + actions: ctx.actions, + + columnMenuVisibleForColumnId: ctx.state.columnMenuVisibleForColumnId, + columnMenuVisibleKey: ctx.state.columnMenuVisibleKey, + }; + }); - const masterContext = useMasterDetailContext(); - const { getState, actions } = context; + const { portalDOMRef } = + useDataSourceMasterDetailSelector((ctx) => { + return { + portalDOMRef: ctx.getMasterState().portalDOMRef, + }; + }) || {}; const { showOverlay, portal: menuPortal, clearAll, } = useOverlay({ - portalContainer: masterContext - ? masterContext.getMasterState().portalDOMRef.current - : false, + portalContainer: portalDOMRef?.current ?? false, }); useEffect(() => { - const { actions: actions, getState } = context; const state = getState(); return state.onColumnMenuClick.onChange((info) => { @@ -98,8 +118,6 @@ export function useColumnMenu() { }); }, []); - const { columnMenuVisibleForColumnId, columnMenuVisibleKey } = getState(); - useEffect(() => { const { columnMenuVisibleForColumnId, columnMenuTargetRef } = getState(); diff --git a/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts b/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts index 74bd3a768..15b67fc8c 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnPointerEvents.ts @@ -1,7 +1,6 @@ import * as React from 'react'; import { useCallback, useState } from 'react'; - -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; import { clearInfiniteColumnReorderDuration, @@ -62,7 +61,7 @@ export const useColumnPointerEvents = ({ }: { allowColumnHideOnDrag: boolean; columnId: string; - domRef: React.MutableRefObject; + domRef: React.RefObject; horizontalLayoutPageIndex: number | null; }) => { const [proxyPosition, setProxyPosition] = useState(null); @@ -70,14 +69,19 @@ export const useColumnPointerEvents = ({ const { getCurrentDragSourceAndTarget } = useDragDropProvider(); - const { - actions, - computed, - getComputed, - getState, - api, - state: { domRef: rootRef }, - } = useInfiniteTable(); + const { getComputed, getState, api, computedDraggable, rootRef, actions } = + useInfiniteTableSelector((ctx) => { + return { + actions: ctx.actions, + getComputed: ctx.getComputed, + getState: ctx.getState, + api: ctx.api, + computedDraggable: + ctx.computed.computedVisibleColumnsMap.get(columnId) + ?.computedDraggable, + rootRef: ctx.state.domRef, + }; + }); const defaultPointerDown = useCallback((e: React.PointerEvent) => { const computedCol = getComputed().computedVisibleColumnsMap.get(columnId); @@ -363,10 +367,7 @@ export const useColumnPointerEvents = ({ ); return { - onPointerDown: computed.computedVisibleColumnsMap.get(columnId) - ?.computedDraggable - ? onPointerDown - : defaultPointerDown, + onPointerDown: computedDraggable ? onPointerDown : defaultPointerDown, dragColumnOutside, diff --git a/source/src/components/InfiniteTable/hooks/useColumnRowspan.ts b/source/src/components/InfiniteTable/hooks/useColumnRowspan.ts index 992afd8b8..12c54e414 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnRowspan.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnRowspan.ts @@ -1,13 +1,17 @@ import { useMemo } from 'react'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; import { MatrixBrainOptions } from '../../VirtualBrain/MatrixBrain'; import { InfiniteTableComputedColumn } from '../types'; +import { DataSourceState, useDataSourceSelector } from '../../DataSource'; export function useColumnRowspan( computedVisibleColumns: InfiniteTableComputedColumn[], ) { - const { getState: getDataSourceState } = useDataSourceContextValue(); + const { getDataSourceState } = useDataSourceSelector((ctx) => { + return { + getDataSourceState: ctx.getDataSourceState as () => DataSourceState, + }; + }); const rowspan = useMemo(() => { const colsWithRowspan = computedVisibleColumns.filter( diff --git a/source/src/components/InfiniteTable/hooks/useColumnsWhen.ts b/source/src/components/InfiniteTable/hooks/useColumnsWhen.ts index 6460ca43e..206d0414c 100644 --- a/source/src/components/InfiniteTable/hooks/useColumnsWhen.ts +++ b/source/src/components/InfiniteTable/hooks/useColumnsWhen.ts @@ -1,14 +1,19 @@ import { useEffect, useLayoutEffect, useMemo } from 'react'; -import type { InfiniteTableColumn, InfiniteTableState } from '..'; -import { shallowEqualObjects } from '../../../utils/shallowEqualObjects'; import type { - DataSourceGroupBy, - DataSourcePivotBy, - DataSourcePropGroupBy, - DataSourcePropSelectionMode, + InfiniteTableColumn, + InfiniteTableRowInfo, + InfiniteTableState, +} from '..'; +import { shallowEqualObjects } from '../../../utils/shallowEqualObjects'; +import { + DataSourceState, + useDataSourceSelector, + type DataSourceGroupBy, + type DataSourcePivotBy, + type DataSourcePropGroupBy, + type DataSourcePropSelectionMode, } from '../../DataSource'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; import { useManagedComponentState } from '../../hooks/useComponentState'; import { getGroupByMap } from '../state/getInitialState'; import { @@ -25,7 +30,7 @@ import type { InfiniteTablePropGroupRenderStrategy, InfiniteTableProps, } from '../types/InfiniteTableProps'; -import { GroupByMap } from '../types/InfiniteTableState'; +import { GroupByMap, InfiniteTableActions } from '../types/InfiniteTableState'; import { getColumnForGroupBy, getGroupColumnRender, @@ -34,24 +39,28 @@ import { } from '../utils/getColumnForGroupBy'; import { ToggleGroupRowFn, useToggleGroupRow } from './useToggleGroupRow'; +import { useLatest } from '../../hooks/useLatest'; function useGroupByMap(groupBy: DataSourcePropGroupBy) { return useMemo(() => getGroupByMap(groupBy), [groupBy]); } -export function useColumnsWhen() { - const { - componentState: { groupBy }, - componentActions: dataSourceActions, - } = useDataSourceContextValue(); +export function useColumnsWhen( + state: InfiniteTableState, + actions: InfiniteTableActions, +) { + const { groupBy, dataSourceActions } = useDataSourceSelector((ctx) => { + return { + groupBy: ctx.dataSourceState.groupBy as DataSourceGroupBy[], + dataSourceActions: ctx.dataSourceActions, + }; + }); const { - componentState: { - groupRenderStrategy, - pivotTotalColumnPosition, - pivotGrandTotalColumnPosition, - }, - } = useManagedComponentState>(); + groupRenderStrategy, + pivotTotalColumnPosition, + pivotGrandTotalColumnPosition, + } = state; useEffect(() => { dataSourceActions.generateGroupRows = groupRenderStrategy !== 'inline'; @@ -70,21 +79,24 @@ export function useColumnsWhen() { const groupByMap = useGroupByMap(groupBy); - useColumnsWhenInlineGroupRenderStrategy(groupByMap); + useColumnsWhenInlineGroupRenderStrategy(groupByMap, state, actions); const { toggleGroupRow } = useColumnsWhenGrouping(); - useHideColumns(groupByMap); + useHideColumns(groupByMap, state, actions); return { toggleGroupRow }; } -function useColumnsWhenInlineGroupRenderStrategy(groupByMap: GroupByMap) { +function useColumnsWhenInlineGroupRenderStrategy( + groupByMap: GroupByMap, + state: InfiniteTableState, + actions: InfiniteTableActions, +) { const toggleGroupRow = useToggleGroupRow(); - const { - componentActions, - componentState: { columns, groupRenderStrategy, isTree }, - } = useManagedComponentState>(); + const { columns, groupRenderStrategy, isTree } = state; + + const componentActions = actions; function computeColumnsWhenInlineGroupRenderStrategy( columns: Record>, @@ -194,9 +206,13 @@ function useColumnsWhenInlineGroupRenderStrategy(groupByMap: GroupByMap) { } function useColumnsWhenGrouping() { - const { - componentState: { groupBy, pivotBy, selectionMode }, - } = useDataSourceContextValue(); + const { groupBy, pivotBy, selectionMode } = useDataSourceSelector((ctx) => { + return { + groupBy: ctx.dataSourceState.groupBy as DataSourceGroupBy[], + pivotBy: ctx.dataSourceState.pivotBy as DataSourcePivotBy[], + selectionMode: ctx.dataSourceState.selectionMode, + }; + }); const { componentActions, @@ -279,31 +295,44 @@ function useColumnsWhenGrouping() { return { toggleGroupRow }; } -function useHideColumns(groupByMap: GroupByMap) { +function useHideColumns( + groupByMap: GroupByMap, + state: InfiniteTableState, + actions: InfiniteTableActions, +) { const { - componentState: { - dataArray, - groupRowsIndexesInDataArray, - groupBy, - groupRowsState, - originalLazyGroupDataChangeDetect, - }, - getState: getDataSourceState, - } = useDataSourceContextValue(); + dataArray, + groupRowsIndexesInDataArray, + groupBy, + groupRowsState, + originalLazyGroupDataChangeDetect, + getDataSourceState, + } = useDataSourceSelector((ctx) => { + return { + getDataSourceState: ctx.getDataSourceState, + dataArray: ctx.dataSourceState.dataArray as InfiniteTableRowInfo[], + groupRowsIndexesInDataArray: + ctx.dataSourceState.groupRowsIndexesInDataArray, + groupBy: ctx.dataSourceState.groupBy as DataSourceState['groupBy'], + groupRowsState: ctx.dataSourceState.groupRowsState, + originalLazyGroupDataChangeDetect: + ctx.dataSourceState.originalLazyGroupDataChangeDetect, + }; + }); const { - getComponentState, - componentActions, - componentState: { - columnTypes, - computedColumns, + columnTypes, + computedColumns, - hideColumnWhenGrouped, - hideEmptyGroupColumns, + hideColumnWhenGrouped, + hideEmptyGroupColumns, - groupRenderStrategy, - }, - } = useManagedComponentState>(); + groupRenderStrategy, + } = state; + + const componentActions = actions; + + const getComponentState = useLatest(state); // implements hideEmptyGroupColumns useLayoutEffect(() => { @@ -317,7 +346,7 @@ function useHideColumns(groupByMap: GroupByMap) { groupByMap, ); - const newColumnVisibility = getColumnVisibilityForHideEmptyGroupColumns({ + const newColumnVisibility = getColumnVisibilityForHideEmptyGroupColumns({ computedGroupColumns, columnVisibility: currentState.columnVisibility, hideEmptyGroupColumns, diff --git a/source/src/components/InfiniteTable/hooks/useComputed.ts b/source/src/components/InfiniteTable/hooks/useComputed.ts index 757151814..c9e9b0f9b 100644 --- a/source/src/components/InfiniteTable/hooks/useComputed.ts +++ b/source/src/components/InfiniteTable/hooks/useComputed.ts @@ -1,7 +1,5 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; -import { useManagedComponentState } from '../../hooks/useComponentState'; import { useLatest } from '../../hooks/useLatest'; import type { InfiniteTableState, InfiniteTableComputedValues } from '../types'; import { MultiCellSelector } from '../utils/MultiCellSelector'; @@ -13,20 +11,27 @@ import { useColumnsWhen } from './useColumnsWhen'; import { useComputedColumns } from './useComputedColumns'; import { useComputedRowHeight } from './useComputedRowHeight'; import { useScrollbars } from './useScrollbars'; +import { DataSourceState, useDataSourceSelector } from '../../DataSource'; +import { InfiniteTableActions } from '../types/InfiniteTableState'; -export function useComputed(): InfiniteTableComputedValues { - const { componentActions, componentState } = - useManagedComponentState>(); - - const { - componentActions: dataSourceActions, - componentState: dataSourceState, - getState: getDataSourceState, - api: dataSourceApi, - } = useDataSourceContextValue(); - +export function useComputed(options: { + state: InfiniteTableState; + actions: InfiniteTableActions; + getState: () => InfiniteTableState; +}): InfiniteTableComputedValues { + const { state, actions, getState } = options; const { columnOrder, + columnCssEllipsis, + columnHeaderCssEllipsis, + columnMinWidth, + columnMaxWidth, + columnDefaultWidth, + columnDefaultFlex, + columnDefaultSortable, + columnDefaultDraggable, + pinnedStartMaxWidth, + pinnedEndMaxWidth, columnVisibility, columnPinning, columnSizing, @@ -39,37 +44,67 @@ export function useComputed(): InfiniteTableComputedValues { rowDetailHeight, isRowDetailExpanded: isRowDetailsExpanded, isRowDetailEnabled: isRowDetailsEnabled, - brain, bodySize, showSeparatePivotColumnForSingleAggregation, - } = componentState; + onRowHeightCSSVarChange, + onFlashingDurationCSSVarChange, + onRowDetailHeightCSSVarChange, + onColumnHeaderHeightCSSVarChange, + + computedColumns, + viewportReservedWidth, + resizableColumns, + sortable, + draggableColumns, + } = state; + + const componentActions = actions; + + const { + multiSort, + filterValue, + filterTypes, + groupBy, + sortInfo, + dataSourceActions, + getDataSourceState, + dataSourceApi, + } = useDataSourceSelector((ctx) => { + return { + dataSourceActions: ctx.dataSourceActions, + getDataSourceState: ctx.getDataSourceState as () => DataSourceState, + dataSourceApi: ctx.dataSourceApi, + sortInfo: ctx.dataSourceState.sortInfo as DataSourceState['sortInfo'], + multiSort: ctx.dataSourceState.multiSort, + filterValue: ctx.dataSourceState + .filterValue as DataSourceState['filterValue'], + filterTypes: ctx.dataSourceState.filterTypes, + groupBy: ctx.dataSourceState.groupBy as DataSourceState['groupBy'], + }; + }); useState(() => { - componentState.onRowHeightCSSVarChange.onChange((rowHeight) => { + onRowHeightCSSVarChange.onChange((rowHeight) => { if (rowHeight) { componentActions.rowHeight = rowHeight; } }); - componentState.onFlashingDurationCSSVarChange.onChange( - (flashingDuration) => { - const num = flashingDuration ? flashingDuration * 1 : null; - if (num != null && !isNaN(num)) { - componentActions.flashingDurationCSSVarValue = num; - } - }, - ); - componentState.onRowDetailHeightCSSVarChange.onChange((rowDetailHeight) => { + onFlashingDurationCSSVarChange.onChange((flashingDuration) => { + const num = flashingDuration ? flashingDuration * 1 : null; + if (num != null && !isNaN(num)) { + componentActions.flashingDurationCSSVarValue = num; + } + }); + onRowDetailHeightCSSVarChange.onChange((rowDetailHeight) => { if (rowDetailHeight) { componentActions.rowDetailHeight = rowDetailHeight; } }); - componentState.onColumnHeaderHeightCSSVarChange.onChange( - (columnHeaderHeight) => { - if (columnHeaderHeight) { - componentActions.columnHeaderHeight = columnHeaderHeight; - } - }, - ); + onColumnHeaderHeightCSSVarChange.onChange((columnHeaderHeight) => { + if (columnHeaderHeight) { + componentActions.columnHeaderHeight = columnHeaderHeight; + } + }); }); useEffect(() => { @@ -77,11 +112,11 @@ export function useComputed(): InfiniteTableComputedValues { showSeparatePivotColumnForSingleAggregation; }, [showSeparatePivotColumnForSingleAggregation]); - const { multiSort, filterValue, filterTypes, groupBy } = dataSourceState; + // const { multiSort, filterValue, filterTypes, groupBy } = dataSourceState; - const { toggleGroupRow } = useColumnsWhen(); + const { toggleGroupRow } = useColumnsWhen(state, actions); - const columns = componentState.computedColumns; + const columns = computedColumns; const { computedColumnOrder, @@ -112,24 +147,24 @@ export function useComputed(): InfiniteTableComputedValues { // can cause a horizontal scrollbar which in turn causes a vertical scrollbar and the scenario // can loop so it's safer for now to always reserve space for the scrollbar scrollbarWidth: undefined, - columnCssEllipsis: componentState.columnCssEllipsis, - columnHeaderCssEllipsis: componentState.columnHeaderCssEllipsis, - columnMinWidth: componentState.columnMinWidth, - columnMaxWidth: componentState.columnMaxWidth, - columnDefaultWidth: componentState.columnDefaultWidth, - columnDefaultFlex: componentState.columnDefaultFlex, - columnDefaultSortable: componentState.columnDefaultSortable, - columnDefaultDraggable: componentState.columnDefaultDraggable, - pinnedStartMaxWidth: componentState.pinnedStartMaxWidth, - pinnedEndMaxWidth: componentState.pinnedEndMaxWidth, + columnCssEllipsis, + columnHeaderCssEllipsis, + columnMinWidth, + columnMaxWidth, + columnDefaultWidth, + columnDefaultFlex, + columnDefaultSortable, + columnDefaultDraggable, + pinnedStartMaxWidth, + pinnedEndMaxWidth, bodySize, - viewportReservedWidth: componentState.viewportReservedWidth, - resizableColumns: componentState.resizableColumns, + viewportReservedWidth, + resizableColumns, - sortable: componentState.sortable, - draggableColumns: componentState.draggableColumns, - sortInfo: dataSourceState.sortInfo ?? undefined, + sortable, + draggableColumns, + sortInfo: sortInfo ?? undefined, multiSort, groupBy, @@ -152,7 +187,7 @@ export function useComputed(): InfiniteTableComputedValues { const rowspan = useColumnRowspan(computedVisibleColumns); const columnSize = useColumnSizeFn(computedVisibleColumns); - const scrollbars = useScrollbars(brain); + const scrollbars = useScrollbars(getState); const computedPinnedStartOverflow = computedPinnedStartWidth ? computedPinnedStartColumnsWidth > computedPinnedStartWidth @@ -197,35 +232,67 @@ export function useComputed(): InfiniteTableComputedValues { getDataSourceState, }); - return { - multiRowSelector, - multiCellSelector, - computedRowSizeCacheForDetails, - computedRowHeight, - scrollbars, - columnSize, - rowspan, - toggleGroupRow, - computedColumnsMap, - computedColumnsMapInInitialOrder, - renderSelectionCheckBox, - computedPinnedStartOverflow, - computedPinnedEndOverflow, - computedPinnedStartWidth, - computedPinnedEndWidth, - computedVisibleColumns, - computedColumnOrder, - computedRemainingSpace, - computedVisibleColumnsMap, - // computedColumnVisibility: columnVisibility, - computedPinnedStartColumns, - computedPinnedEndColumns, - computedUnpinnedColumns, - computedPinnedStartColumnsWidth, - computedPinnedEndColumnsWidth, - computedUnpinnedColumnsWidth, - computedUnpinnedOffset, - computedPinnedEndOffset, - fieldsToColumn, - }; + return useMemo( + () => ({ + multiRowSelector, + multiCellSelector, + computedRowSizeCacheForDetails, + computedRowHeight, + scrollbars, + columnSize, + rowspan, + toggleGroupRow, + computedColumnsMap, + computedColumnsMapInInitialOrder, + renderSelectionCheckBox, + computedPinnedStartOverflow, + computedPinnedEndOverflow, + computedPinnedStartWidth, + computedPinnedEndWidth, + computedVisibleColumns, + computedColumnOrder, + computedRemainingSpace, + computedVisibleColumnsMap, + // computedColumnVisibility: columnVisibility, + computedPinnedStartColumns, + computedPinnedEndColumns, + computedUnpinnedColumns, + computedPinnedStartColumnsWidth, + computedPinnedEndColumnsWidth, + computedUnpinnedColumnsWidth, + computedUnpinnedOffset, + computedPinnedEndOffset, + fieldsToColumn, + }), + [ + multiRowSelector, + multiCellSelector, + computedRowSizeCacheForDetails, + computedRowHeight, + scrollbars, + columnSize, + rowspan, + toggleGroupRow, + computedColumnsMap, + computedColumnsMapInInitialOrder, + renderSelectionCheckBox, + computedPinnedStartOverflow, + computedPinnedEndOverflow, + computedPinnedStartWidth, + computedPinnedEndWidth, + computedVisibleColumns, + computedColumnOrder, + computedRemainingSpace, + computedVisibleColumnsMap, + computedPinnedStartColumns, + computedPinnedEndColumns, + computedUnpinnedColumns, + computedPinnedStartColumnsWidth, + computedPinnedEndColumnsWidth, + computedUnpinnedColumnsWidth, + computedUnpinnedOffset, + computedPinnedEndOffset, + fieldsToColumn, + ], + ); } diff --git a/source/src/components/InfiniteTable/hooks/useContextMenu.ts b/source/src/components/InfiniteTable/hooks/useContextMenu.ts index 9b07b08e3..2f1e09bc9 100644 --- a/source/src/components/InfiniteTable/hooks/useContextMenu.ts +++ b/source/src/components/InfiniteTable/hooks/useContextMenu.ts @@ -1,14 +1,14 @@ import { useEffect } from 'react'; import { AlignPositionOptions } from '../../../utils/pageGeometry/alignment'; import { Rectangle } from '../../../utils/pageGeometry/Rectangle'; -import { useMasterDetailContext } from '../../DataSource/publicHooks/useDataSourceState'; import { useOverlay } from '../../hooks/useOverlay'; import { getCellContextMenu, getTableContextMenu, } from '../utils/getCellContextMenu'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableStableContext } from './useInfiniteTableSelector'; +import { useDataSourceMasterDetailSelector } from '../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; const OFFSET = 5; @@ -24,17 +24,20 @@ const ALIGN_POSITIONS: AlignPositionOptions['alignPosition'] = [ ]; export function useCellContextMenu() { - const context = useInfiniteTable(); - const masterContext = useMasterDetailContext(); + const context = useInfiniteTableStableContext(); + const { portalDOMRef } = + useDataSourceMasterDetailSelector((ctx) => { + return { + portalDOMRef: ctx.getMasterState().portalDOMRef, + }; + }) || {}; const { getState, actions } = context; const { showOverlay, portal: menuPortal, clearAll, } = useOverlay({ - portalContainer: masterContext - ? masterContext.getMasterState().portalDOMRef.current - : false, + portalContainer: portalDOMRef?.current ?? false, }); useEffect(() => { @@ -127,17 +130,21 @@ export function useCellContextMenu() { } export function useTableContextMenu() { - const context = useInfiniteTable(); - const masterContext = useMasterDetailContext(); + const context = useInfiniteTableStableContext(); + + const { portalDOMRef } = + useDataSourceMasterDetailSelector((ctx) => { + return { + portalDOMRef: ctx.getMasterState().portalDOMRef, + }; + }) || {}; const { getState, actions } = context; const { showOverlay, portal: menuPortal, clearAll, } = useOverlay({ - portalContainer: masterContext - ? masterContext.getMasterState().portalDOMRef.current - : false, + portalContainer: portalDOMRef?.current ?? false, }); useEffect(() => { diff --git a/source/src/components/InfiniteTable/hooks/useDOMProps.ts b/source/src/components/InfiniteTable/hooks/useDOMProps.ts index e4ecb7547..e31c14db1 100644 --- a/source/src/components/InfiniteTable/hooks/useDOMProps.ts +++ b/source/src/components/InfiniteTable/hooks/useDOMProps.ts @@ -14,7 +14,7 @@ import { InfiniteTableComputedColumn } from '../types'; import { InternalVarUtils } from '../utils/infiniteDOMUtils'; import { rafFn } from '../utils/rafFn'; import { ThemeVars } from '../vars.css'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; const publicRuntimeVars: Record< keyof typeof ThemeVars.runtime, @@ -104,8 +104,10 @@ export function useDOMProps( ) { const scrollbarWidth = getScrollbarWidth(); - const { computed, state, actions, getState } = useInfiniteTable(); const { + actions, + getState, + focused, focusedWithin, domRef, @@ -117,8 +119,6 @@ export function useDOMProps( bodySize, activeCellIndex, wrapRowsHorizontally, - } = state; - const { computedPinnedStartColumnsWidth, computedPinnedEndColumnsWidth, computedPinnedStartColumns, @@ -126,7 +126,57 @@ export function useDOMProps( computedVisibleColumns, computedUnpinnedColumnsWidth, scrollbars, - } = computed; + + focusedClassName, + focusedWithinClassName, + computedPinnedStartOverflow, + computedPinnedEndOverflow, + + focusedStyle, + focusedWithinStyle, + + brain, + } = useInfiniteTableSelector((ctx) => { + const { state, computed } = ctx; + return { + brain: state.brain, + actions: ctx.actions, + getState: ctx.getState, + focused: state.focused, + focusedWithin: state.focusedWithin, + domRef: state.domRef, + scrollerDOMRef: state.scrollerDOMRef, + onBlurWithin: state.onBlurWithin, + onFocusWithin: state.onFocusWithin, + onSelfFocus: state.onSelfFocus, + onSelfBlur: state.onSelfBlur, + bodySize: state.bodySize, + activeCellIndex: state.activeCellIndex, + wrapRowsHorizontally: state.wrapRowsHorizontally, + + computedPinnedStartColumnsWidth: computed.computedPinnedStartColumnsWidth, + computedPinnedEndColumnsWidth: computed.computedPinnedEndColumnsWidth, + + computedPinnedStartColumns: + computed.computedPinnedStartColumns as InfiniteTableComputedColumn[], + computedPinnedEndColumns: + computed.computedPinnedEndColumns as InfiniteTableComputedColumn[], + + computedVisibleColumns: + computed.computedVisibleColumns as InfiniteTableComputedColumn[], + + computedUnpinnedColumnsWidth: computed.computedUnpinnedColumnsWidth, + + scrollbars: computed.scrollbars, + + focusedClassName: ctx.state.focusedClassName, + focusedWithinClassName: ctx.state.focusedWithinClassName, + computedPinnedStartOverflow: computed.computedPinnedStartOverflow, + computedPinnedEndOverflow: computed.computedPinnedEndOverflow, + focusedStyle: ctx.state.focusedStyle, + focusedWithinStyle: ctx.state.focusedWithinStyle, + }; + }); const prevPinnedEndCols: InfiniteTableComputedColumn[] = []; const cssVars: CSSProperties = computedVisibleColumns.reduce( @@ -212,8 +262,8 @@ export function useDOMProps( const defaultActiveCellColOffset = InternalVarUtils.columnOffsets.get( activeCellIndex[1], ); - if (state.brain.isHorizontalLayoutBrain) { - const pageIndex = state.brain.getPageIndexForRow(activeCellIndex[0]); + if (brain.isHorizontalLayoutBrain) { + const pageIndex = brain.getPageIndexForRow(activeCellIndex[0]); //@ts-ignore cssVars[activeCellColOffset] = pageIndex @@ -347,14 +397,12 @@ export function useDOMProps( focused ? `${InfiniteTableClassName}--focused` : null, focusedWithin ? `${InfiniteTableClassName}--focused-within` : null, - focused && state.focusedClassName ? state.focusedClassName : null, - focusedWithin && state.focusedWithinClassName - ? state.focusedWithinClassName - : null, - computed.computedPinnedStartOverflow + focused && focusedClassName ? focusedClassName : null, + focusedWithin && focusedWithinClassName ? focusedWithinClassName : null, + computedPinnedStartOverflow ? `${InfiniteTableClassName}--has-pinned-start-overflow` : null, - computed.computedPinnedEndOverflow + computedPinnedEndOverflow ? `${InfiniteTableClassName}--has-pinned-end-overflow` : null, @@ -376,13 +424,13 @@ export function useDOMProps( ...cssVars, }; if (focused) { - if (state.focusedStyle) { - Object.assign(domProps.style, state.focusedStyle); + if (focusedStyle) { + Object.assign(domProps.style, focusedStyle); } } if (focusedWithin) { - if (state.focusedWithinStyle) { - Object.assign(domProps.style, state.focusedWithinStyle); + if (focusedWithinStyle) { + Object.assign(domProps.style, focusedWithinStyle); } } diff --git a/source/src/components/InfiniteTable/hooks/useDebugMode.ts b/source/src/components/InfiniteTable/hooks/useDebugMode.ts index f48ee773f..4b9bc96c2 100644 --- a/source/src/components/InfiniteTable/hooks/useDebugMode.ts +++ b/source/src/components/InfiniteTable/hooks/useDebugMode.ts @@ -6,7 +6,6 @@ import { debug } from '../../../utils/debugPackage'; import { stripVar } from '../../../utils/stripVar'; import { CSS_LOADED_VALUE, ThemeVars } from '../vars.css'; -import { useInfiniteTable } from './useInfiniteTable'; import { DataSourceDebugWarningKey, ErrorCodeKey, @@ -44,6 +43,7 @@ import { DevToolsOverlayBg, DevToolsOverlayText, } from './debugModeDevToolsOverlay.css'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; const cssFileLoadedVarName = stripVar(ThemeVars.loaded); const messageBase = { @@ -211,7 +211,13 @@ const setupHook = once(() => { export function useDebugMode() { const { getState, getDataSourceState, dataSourceActions } = - useInfiniteTable(); + useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState, + getDataSourceState: ctx.getDataSourceState, + dataSourceActions: ctx.dataSourceActions, + }; + }); const state = getState(); const { domRef, debugId } = state; @@ -510,7 +516,17 @@ export function useDevTools() { actions, dataSourceApi, api, - } = useInfiniteTable(); + } = useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState, + getComputed: ctx.getComputed, + getDataSourceState: ctx.getDataSourceState, + dataSourceActions: ctx.dataSourceActions, + actions: ctx.actions, + dataSourceApi: ctx.dataSourceApi, + api: ctx.api, + }; + }); const state = getState(); diff --git a/source/src/components/InfiniteTable/hooks/useEditingCallbackProps.ts b/source/src/components/InfiniteTable/hooks/useEditingCallbackProps.ts index 20ca44f82..10033e10b 100644 --- a/source/src/components/InfiniteTable/hooks/useEditingCallbackProps.ts +++ b/source/src/components/InfiniteTable/hooks/useEditingCallbackProps.ts @@ -2,15 +2,23 @@ import { useEffect } from 'react'; import { usePrevious } from '../../hooks/usePrevious'; import { getCellContext } from '../components/InfiniteTableRow/columnRendering'; -import { InfiniteTableContextValue } from '../types'; +import { InfiniteTableState } from '../types'; -import { useInfiniteTable } from './useInfiniteTable'; +import { + useInfiniteTableSelector, + useInfiniteTableStableContext, +} from './useInfiniteTableSelector'; +import { InfiniteTableStableContextValue } from '../types/InfiniteTableContextValue'; function useOnEditCancelled() { - const context = useInfiniteTable(); + const context = useInfiniteTableStableContext(); - const { getState } = context; - const { editingCell } = getState(); + const { getState, editingCell } = useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState as () => InfiniteTableState, + editingCell: ctx.state.editingCell, + }; + }); const cancelled = editingCell && !editingCell.active ? editingCell.cancelled : undefined; @@ -33,12 +41,15 @@ function useOnEditCancelled() { } function useOnEditRejected() { - const context: InfiniteTableContextValue = useInfiniteTable(); + const context: InfiniteTableStableContextValue = + useInfiniteTableStableContext(); - const { - getState, - state: { editingCell }, - } = context; + const { getState, editingCell } = useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState as () => InfiniteTableState, + editingCell: ctx.state.editingCell, + }; + }); const rejected = editingCell && !editingCell.active && editingCell.accepted instanceof Error @@ -66,11 +77,14 @@ function useOnEditRejected() { } function useFocusOnEditStop() { - const context: InfiniteTableContextValue = useInfiniteTable(); + const context: InfiniteTableStableContextValue = + useInfiniteTableStableContext(); - const { - state: { editingCell }, - } = context; + const { editingCell } = useInfiniteTableSelector((ctx) => { + return { + editingCell: ctx.state.editingCell, + }; + }); const active = editingCell?.active; const prevActive = usePrevious(active); @@ -83,12 +97,15 @@ function useFocusOnEditStop() { } function useOnEditAccepted() { - const context: InfiniteTableContextValue = useInfiniteTable(); + const context: InfiniteTableStableContextValue = + useInfiniteTableStableContext(); - const { - state: { editingCell }, - getState, - } = context; + const { editingCell, getState } = useInfiniteTableSelector((ctx) => { + return { + editingCell: ctx.state.editingCell, + getState: ctx.getState as () => InfiniteTableState, + }; + }); const accepted = editingCell && @@ -118,12 +135,15 @@ function useOnEditAccepted() { } function useOnEditPersisted() { - const context: InfiniteTableContextValue = useInfiniteTable(); - - const { - state: { editingCell }, - getState, - } = context; + const context: InfiniteTableStableContextValue = + useInfiniteTableStableContext(); + + const { editingCell, getState } = useInfiniteTableSelector((ctx) => { + return { + editingCell: ctx.state.editingCell, + getState: ctx.getState as () => InfiniteTableState, + }; + }); const persisted = editingCell ? editingCell.persisted : undefined; diff --git a/source/src/components/InfiniteTable/hooks/useGridScroll.ts b/source/src/components/InfiniteTable/hooks/useGridScroll.ts index c6ba2e3d7..5dbd027fb 100644 --- a/source/src/components/InfiniteTable/hooks/useGridScroll.ts +++ b/source/src/components/InfiniteTable/hooks/useGridScroll.ts @@ -1,14 +1,16 @@ import { useCallback, useEffect } from 'react'; import { ScrollPosition } from '../../types/ScrollPosition'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; export function useGridScroll( onScroll: (scrollPosition: ScrollPosition) => void, deps: any[], ) { - const { - state: { brain }, - } = useInfiniteTable(); + const { brain } = useInfiniteTableSelector((ctx) => { + return { + brain: ctx.state.brain, + }; + }); const memoizedOnScroll = useCallback(onScroll, deps); diff --git a/source/src/components/InfiniteTable/hooks/useHorizontalLayout.ts b/source/src/components/InfiniteTable/hooks/useHorizontalLayout.ts index 45ad3e704..b66a95d90 100644 --- a/source/src/components/InfiniteTable/hooks/useHorizontalLayout.ts +++ b/source/src/components/InfiniteTable/hooks/useHorizontalLayout.ts @@ -1,10 +1,17 @@ import { useEffect } from 'react'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; export function useHorizontalLayout() { const { getState, actions, dataSourceActions, getDataSourceState } = - useInfiniteTable(); + useInfiniteTableSelector((ctx) => { + return { + getState: ctx.getState, + actions: ctx.actions, + dataSourceActions: ctx.dataSourceActions, + getDataSourceState: ctx.getDataSourceState, + }; + }); const { groupBy, isTree } = getDataSourceState(); const state = getState(); diff --git a/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts b/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts index e99b948d1..7b88cc250 100644 --- a/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts +++ b/source/src/components/InfiniteTable/hooks/useInfinitePortalContainer.ts @@ -1,13 +1,19 @@ -import { useMasterDetailContext } from '../../DataSource/publicHooks/useDataSourceState'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useDataSourceMasterDetailSelector } from '../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; export function useInfinitePortalContainer() { - const masterContext = useMasterDetailContext(); + const { masterPortalDOMRef } = + useDataSourceMasterDetailSelector((ctx) => { + return { + masterPortalDOMRef: ctx.getMasterState().portalDOMRef, + }; + }) ?? {}; - const masterState = masterContext ? masterContext.getMasterState() : null; - const infiniteState = useInfiniteTable().getState(); + const portalDOMRef = useInfiniteTableSelector( + (ctx) => ctx.state.portalDOMRef, + ); - const portalContainer = (masterState || infiniteState).portalDOMRef.current; + const portalContainer = masterPortalDOMRef?.current ?? portalDOMRef?.current; return portalContainer; } diff --git a/source/src/components/InfiniteTable/hooks/useInfiniteTable.ts b/source/src/components/InfiniteTable/hooks/useInfiniteTable.ts deleted file mode 100644 index b18011332..000000000 --- a/source/src/components/InfiniteTable/hooks/useInfiniteTable.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useContext } from 'react'; -import type { InfiniteTableContextValue } from '../types'; - -import { getInfiniteTableContext } from '../InfiniteTableContext'; - -export const useInfiniteTable = (): InfiniteTableContextValue => { - const TableContext = getInfiniteTableContext(); - - return useContext(TableContext); -}; diff --git a/source/src/components/InfiniteTable/hooks/useInfiniteTableSelector.ts b/source/src/components/InfiniteTable/hooks/useInfiniteTableSelector.ts new file mode 100644 index 000000000..52df17727 --- /dev/null +++ b/source/src/components/InfiniteTable/hooks/useInfiniteTableSelector.ts @@ -0,0 +1,69 @@ +import { useContext } from 'react'; + +import { getInfiniteTableStoreContext } from '../InfiniteTableContext'; +import type { + InfiniteTableContextValue, + InfiniteTableStableContextValue, +} from '../types/InfiniteTableContextValue'; + +import type { InfiniteTableStore } from '../InfiniteTableStore'; +import { + useComponentStoreSelector, + useComponentStoreSingleValue, +} from '../../../utils/ComponentStore'; + +/** + * A selector hook that reads a single value from the InfiniteTable store. + * Uses Object.is for equality — ideal for primitives and stable references. + * + * Components using this hook only re-render when the selected value + * actually changes, unlike useContext which re-renders on every context update. + * + * @param selector Function that extracts a value from the context. + * Does NOT need to be memoized by the caller. + */ +export function useInfiniteTableSingleValue( + selector: (ctx: InfiniteTableContextValue) => R, +): R { + const StoreContext = getInfiniteTableStoreContext(); + const store = useContext(StoreContext) as InfiniteTableStore; + return useComponentStoreSingleValue(store, selector); +} + +/** + * A selector hook that reads an object from the InfiniteTable store. + * Uses shallow equality — re-renders only when a property of the + * returned object actually changes (by reference). + * + * Use this when the selector returns a derived object (e.g. picking + * multiple properties), so that a new object literal with the same + * values does not trigger a re-render. + * + * @param selector Function that extracts an object from the context. + * Does NOT need to be memoized by the caller. + */ +export function useInfiniteTableSelector( + selector: (ctx: InfiniteTableContextValue) => R, +): R { + const StoreContext = getInfiniteTableStoreContext(); + const store = useContext(StoreContext) as InfiniteTableStore; + return useComponentStoreSelector(store, selector); +} + +export function useInfiniteTableStableContext() { + return useInfiniteTableSelector((ctx) => { + const stableContext: InfiniteTableStableContextValue = { + getState: ctx.getState, + actions: ctx.actions, + getComputed: ctx.getComputed, + getDataSourceState: ctx.getDataSourceState, + dataSourceApi: ctx.dataSourceApi, + dataSourceActions: ctx.dataSourceActions, + api: ctx.api, + + getDataSourceMasterContext: ctx.getDataSourceMasterContext, + }; + + return stableContext; + }); +} diff --git a/source/src/components/InfiniteTable/hooks/useInfiniteTableState.ts b/source/src/components/InfiniteTable/hooks/useInfiniteTableState.ts deleted file mode 100644 index fd8286bc6..000000000 --- a/source/src/components/InfiniteTable/hooks/useInfiniteTableState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { useContext } from 'react'; -import { InfiniteTableState } from '../types'; - -import { getInfiniteTableContext } from '../InfiniteTableContext'; - -export const useInfiniteTableState = (): InfiniteTableState => { - const TableContext = getInfiniteTableContext(); - const { state } = useContext(TableContext); - - return state; -}; diff --git a/source/src/components/InfiniteTable/hooks/useLicense/useLicense.ts b/source/src/components/InfiniteTable/hooks/useLicense/useLicense.ts index c8c60cfb7..47961aae5 100644 --- a/source/src/components/InfiniteTable/hooks/useLicense/useLicense.ts +++ b/source/src/components/InfiniteTable/hooks/useLicense/useLicense.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import { useMasterDetailContext } from '../../../DataSource/publicHooks/useDataSourceState'; import { isValidLicense } from './decode'; +import { useMasterRowInfo } from '../../../DataSource/publicHooks/useDataSourceMasterDetailSelector'; const SANDPACK_REGEX = /(https):\/\/\d+\-\d+\-\d+\-(sandpack\.codesandbox\.io)/g; @@ -25,8 +25,8 @@ const isInsideSandbox = () => { const isInsidePlayground = isInsideSandbox() || isInsideSandpack(); export const useLicense = (licenseKey: string = '') => { - const masterContext = useMasterDetailContext(); - const isDetail = !!masterContext; + const masterRowInfo = useMasterRowInfo(); + const isDetail = !!masterRowInfo; const valid = useMemo(() => { if (isDetail) { diff --git a/source/src/components/InfiniteTable/hooks/useScrollbars.ts b/source/src/components/InfiniteTable/hooks/useScrollbars.ts index 96e01d7c9..a05ca7f6b 100644 --- a/source/src/components/InfiniteTable/hooks/useScrollbars.ts +++ b/source/src/components/InfiniteTable/hooks/useScrollbars.ts @@ -1,19 +1,16 @@ import { useState, useEffect, useLayoutEffect } from 'react'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; -import { useManagedComponentState } from '../../hooks/useComponentState'; -import { MatrixBrain } from '../../VirtualBrain/MatrixBrain'; import { InfiniteTableState, Scrollbars } from '../types'; +import { useDataSourceStableContext } from '../../DataSource/publicHooks/useDataSourceSelector'; const INITIAL_SCROLLBARS: Scrollbars = { vertical: false, horizontal: false, }; -export function useScrollbars(brain: MatrixBrain) { - const { getComponentState: getInfiniteTableState } = - useManagedComponentState>(); - const { getState: getDataSourceState } = useDataSourceContextValue(); +export function useScrollbars(getState: () => InfiniteTableState) { + const brain = getState().brain; + const { getDataSourceState } = useDataSourceStableContext(); const [scrollbars, setScrollbars] = useState(INITIAL_SCROLLBARS); @@ -21,9 +18,14 @@ export function useScrollbars(brain: MatrixBrain) { return brain.onRenderCountChange(() => { const { scrollTopMax, scrollLeftMax } = brain; - setScrollbars({ - vertical: scrollTopMax > 0, - horizontal: scrollLeftMax > 0, + const vertical = scrollTopMax > 0; + const horizontal = scrollLeftMax > 0; + + setScrollbars((prev) => { + if (prev.vertical === vertical && prev.horizontal === horizontal) { + return prev; + } + return { vertical, horizontal }; }); }); }, [brain]); @@ -32,7 +34,7 @@ export function useScrollbars(brain: MatrixBrain) { // this needs to be useLayoutEffect // on live Pagination cursor change we need this - ref #lvpgn const dataSourceState = getDataSourceState(); - const { onScrollbarsChange } = getInfiniteTableState(); + const { onScrollbarsChange } = getState(); const { notifyScrollbarsChange } = dataSourceState; if ( diff --git a/source/src/components/InfiniteTable/hooks/useToggleGroupRow.ts b/source/src/components/InfiniteTable/hooks/useToggleGroupRow.ts index 4d7dc91c6..dfbadc68c 100644 --- a/source/src/components/InfiniteTable/hooks/useToggleGroupRow.ts +++ b/source/src/components/InfiniteTable/hooks/useToggleGroupRow.ts @@ -2,19 +2,25 @@ import { useCallback } from 'react'; import { DeepMap } from '../../../utils/DeepMap'; import { LAZY_ROOT_KEY_FOR_GROUPS } from '../../../utils/groupAndPivot'; -import { GroupRowsState } from '../../DataSource'; +import { + DataSourceState, + GroupRowsState, + useDataSourceSelector, +} from '../../DataSource'; import { getChangeDetect } from '../../DataSource/privateHooks/getChangeDetect'; import { loadData } from '../../DataSource/privateHooks/useLoadData'; -import { useDataSourceContextValue } from '../../DataSource/publicHooks/useDataSourceState'; export type ToggleGroupRowFn = (groupKeys: any[]) => void; export function useToggleGroupRow() { - const { - getState: getDataSourceState, - componentActions: dataSourceActions, - getDataSourceMasterContext, - } = useDataSourceContextValue(); + const { getDataSourceState, dataSourceActions, getDataSourceMasterContext } = + useDataSourceSelector((ctx) => { + return { + getDataSourceState: ctx.getDataSourceState as () => DataSourceState, + dataSourceActions: ctx.dataSourceActions, + getDataSourceMasterContext: ctx.getDataSourceMasterContext, + }; + }); const toggleGroupRow = useCallback((groupKeys: any[]) => { // todo this is duplicated in imperative api diff --git a/source/src/components/InfiniteTable/hooks/useToggleWrapRowsHorizontally.ts b/source/src/components/InfiniteTable/hooks/useToggleWrapRowsHorizontally.ts index 84c02dcc5..38de8cad5 100644 --- a/source/src/components/InfiniteTable/hooks/useToggleWrapRowsHorizontally.ts +++ b/source/src/components/InfiniteTable/hooks/useToggleWrapRowsHorizontally.ts @@ -2,10 +2,16 @@ import { useEffect } from 'react'; import { usePrevious } from '../../hooks/usePrevious'; import { DEBUG_NAME } from '../InfiniteDebugName'; import { createBrains } from '../state/getInitialState'; -import { useInfiniteTable } from './useInfiniteTable'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; export function useToggleWrapRowsHorizontally() { - const { state, getState, actions } = useInfiniteTable(); + const { state, getState, actions } = useInfiniteTableSelector((ctx) => { + return { + state: ctx.state, + getState: ctx.getState, + actions: ctx.actions, + }; + }); const { wrapRowsHorizontally } = state; const oldWrapRowsHorizontally = usePrevious(wrapRowsHorizontally); diff --git a/source/src/components/InfiniteTable/hooks/useVisibleColumnSizes.ts b/source/src/components/InfiniteTable/hooks/useVisibleColumnSizes.ts index 4d0412865..92a3cadf1 100644 --- a/source/src/components/InfiniteTable/hooks/useVisibleColumnSizes.ts +++ b/source/src/components/InfiniteTable/hooks/useVisibleColumnSizes.ts @@ -1,14 +1,20 @@ import { stripVar } from '../../../utils/stripVar'; import { InternalVars } from '../internalVars.css'; -import { useInfiniteTable } from './useInfiniteTable'; +import { InfiniteTableComputedColumn } from '../types'; +import { useInfiniteTableSelector } from './useInfiniteTableSelector'; const columnWidthAtIndex = stripVar(InternalVars.columnWidthAtIndex); const columnOffsetAtIndex = stripVar(InternalVars.columnOffsetAtIndex); -export function useVisibleColumnSizes() { - const context = useInfiniteTable(); +export function useVisibleColumnSizes() { + const { computedVisibleColumns } = useInfiniteTableSelector((ctx) => { + return { + computedVisibleColumns: ctx.computed + .computedVisibleColumns as InfiniteTableComputedColumn[], + }; + }); - return context.computed.computedVisibleColumns.map((column) => { + return computedVisibleColumns.map((column) => { return { id: column.id, width: column.computedWidth, diff --git a/source/src/components/InfiniteTable/index.tsx b/source/src/components/InfiniteTable/index.tsx index 7aa824df0..cc3868fb1 100644 --- a/source/src/components/InfiniteTable/index.tsx +++ b/source/src/components/InfiniteTable/index.tsx @@ -4,11 +4,7 @@ import { RefObject } from 'react'; import { join } from '../../utils/join'; import { CSSNumericVariableWatch } from '../CSSNumericVariableWatch'; -import { - useDataSourceState, - useDataSourceContextValue, - useMasterDetailContext, -} from '../DataSource/publicHooks/useDataSourceState'; +import { useMasterDetailStore } from '../DataSource/publicHooks/useDataSourceMasterDetailSelector'; import { buildManagedComponent, @@ -31,13 +27,16 @@ import { getImperativeApi } from './api/getImperativeApi'; import { useAutoSizeColumns } from './hooks/useAutoSizeColumns'; import { useComputed } from './hooks/useComputed'; import { useDOMProps } from './hooks/useDOMProps'; -import { useInfiniteTable } from './hooks/useInfiniteTable'; import { useLicense } from './hooks/useLicense/useLicense'; import { useScrollToActiveCell } from './hooks/useScrollToActiveCell'; import { useScrollToActiveRow } from './hooks/useScrollToActiveRow'; -import { getInfiniteTableContext } from './InfiniteTableContext'; +import { getInfiniteTableStoreContext } from './InfiniteTableContext'; +import { + createInfiniteTableStore, + InfiniteTableStore, +} from './InfiniteTableStore'; import { internalProps, rootClassName } from './internalProps'; import { @@ -82,6 +81,12 @@ import { DragDropProvider } from './components/draggable'; import { InfiniteTableHeader } from './components/InfiniteTablePublicHeader'; import { InfiniteTableHeaderProps } from './components/InfiniteTablePublicHeader/types'; import { InfiniteTableBody } from './components/InfiniteTableBody'; +import { useInfiniteTableSelector } from './hooks/useInfiniteTableSelector'; +import { + useDataSourceSelector, + useDataSourceStableContext, +} from '../DataSource/publicHooks/useDataSourceSelector'; +import { useDataSourceState } from '../DataSource'; export const InfiniteTableClassName = internalProps.rootClassName; @@ -103,8 +108,10 @@ const { ManagedComponentContextProvider: InfiniteTableRoot } = //@ts-ignore mappedCallbacks: getMappedCallbacks(), - // @ts-ignore - getParentState: () => useDataSourceState(), + + getParentState: () => { + return useDataSourceState((state) => state); + }, debugName: (props: { debugId?: string }) => { return getDebugChannel(props.debugId, DEBUG_NAME); }, @@ -115,15 +122,29 @@ const { ManagedComponentContextProvider: InfiniteTableRoot } = // ) => { export const InfiniteTableComponent = React.memo( function InfiniteTableComponent() { - const context = useInfiniteTable(); - - const masterContext = useMasterDetailContext(); - const { state: componentState, api, children: initialChildren } = context; + const masterDetailStore = useMasterDetailStore(); + const { componentState, api, initialChildren } = useInfiniteTableSelector( + (ctx) => { + return { + componentState: ctx.state, + api: ctx.api, + initialChildren: ctx.children, + }; + }, + ); const { - componentState: { dataArray }, - getState: getDataSourceState, - componentActions: dataSourceActions, - } = useDataSourceContextValue(); + dataArrayLength, + getDataSourceState, + dataSourceActions, + dataSourceApi, + } = useDataSourceSelector((ctx) => { + return { + dataArrayLength: ctx.dataSourceState.dataArray.length, + getDataSourceState: ctx.getDataSourceState, + dataSourceActions: ctx.dataSourceActions, + dataSourceApi: ctx.dataSourceApi, + }; + }); const { domRef, @@ -147,8 +168,8 @@ export const InfiniteTableComponent = React.memo( licenseKey = InfiniteTable.licenseKey; } - useScrollToActiveRow(activeRowIndex, dataArray.length, api); - useScrollToActiveCell(activeCellIndex, dataArray.length, api); + useScrollToActiveRow(activeRowIndex, dataArrayLength, api); + useScrollToActiveCell(activeCellIndex, dataArrayLength, api); // useRowSelection(); const { onKeyDown } = useDOMEventHandlers(); @@ -208,15 +229,15 @@ export const InfiniteTableComponent = React.memo( ).__DO_NOT_USE_UNLESS_YOU_KNOW_WHAT_YOURE_DOING_IS_READY( componentState.id, componentState.ready, - context.api, - context, + api, + { dataSourceApi }, ); } if (__DEV__) { - (globalThis as any).infiniteApi = context.api; + (globalThis as any).infiniteApi = api; } - }, [componentState.ready, context.api]); + }, [componentState.ready, api, dataSourceApi]); const debugId = useDebugMode(); @@ -226,7 +247,8 @@ export const InfiniteTableComponent = React.memo( // if we are a detail grid, we want to use the master grid's portal // so menus are rendered in the container of the top-most (master) grid - since we can // have multiple levels of nesting - if (masterContext) { + if (masterDetailStore) { + const masterContext = masterDetailStore.getSnapshot(); portalDOMRef.current = masterContext.getMasterState().portalDOMRef.current; } @@ -299,20 +321,26 @@ export const InfiniteTableComponent = React.memo( ); function InfiniteTableContextProvider({ children, + store, }: { children?: Renderable; + store: InfiniteTableStore; }) { const { componentActions, componentState } = useManagedComponentState>(); const { scrollerDOMRef, scrollTopKey } = componentState; - const computed = useComputed(); - const getComputed = useLatest(computed); const getState = useLatest(componentState); + const computed = useComputed({ + state: componentState, + actions: componentActions, + getState, + }); + const getComputed = useLatest(computed); - const masterContext = useMasterDetailContext(); - if (__DEV__ && !masterContext) { + const masterDetailStore = useMasterDetailStore(); + if (__DEV__ && !masterDetailStore) { (globalThis as any).getState = getState; (globalThis as any).getComputed = getComputed; (globalThis as any).componentActions = componentActions; @@ -333,11 +361,11 @@ function InfiniteTableContextProvider({ } const { - getState: getDataSourceState, - componentActions: dataSourceActions, + getDataSourceState, + dataSourceActions, getDataSourceMasterContext, - api: dataSourceApi, - } = useDataSourceContextValue(); + dataSourceApi, + } = useDataSourceStableContext(); const [imperativeApi] = React.useState(() => { return getImperativeApi({ @@ -365,6 +393,18 @@ function InfiniteTableContextProvider({ children, }; + store.setSnapshot(contextValue); + + // Notify subscribers before the browser paints so that + // selector-based components re-render in the same frame, + // avoiding visual tearing. + React.useLayoutEffect(() => { + store.notify(); + }); + // queueMicrotask(() => { + // store.notify(); + // }); + useResizeObserver( scrollerDOMRef, (size) => { @@ -387,13 +427,7 @@ function InfiniteTableContextProvider({ } }, [scrollTopKey, scrollerDOMRef]); - const TableContext = getInfiniteTableContext(); - - return ( - - - - ); + return ; } const DEFAULT_ROW_HEIGHT = 40; @@ -413,6 +447,10 @@ type InfiniteTableComponent = { const InfiniteTable: InfiniteTableComponent = function ( props: InfiniteTableProps, ) { + // Store for useSyncExternalStore-based selectors. + // The store reference is stable (created once), only the snapshot updates. + const [store] = React.useState(() => createInfiniteTableStore()); + const TableStoreContext = getInfiniteTableStoreContext(); const table = ( //@ts-ignore ( {...props} > - + + + ); diff --git a/source/src/components/InfiniteTable/types/InfiniteTableContextValue.ts b/source/src/components/InfiniteTable/types/InfiniteTableContextValue.ts index 83bcf7b84..aa428d33c 100644 --- a/source/src/components/InfiniteTable/types/InfiniteTableContextValue.ts +++ b/source/src/components/InfiniteTable/types/InfiniteTableContextValue.ts @@ -11,14 +11,19 @@ import { OnCellClickContext } from '../eventHandlers/onCellClick'; import { InfiniteTableComputedColumn } from './InfiniteTableColumn'; import { InfiniteTableRowInfoDataDiscriminator } from '../../../utils/groupAndPivot'; -export interface InfiniteTableContextValue { +export interface InfiniteTableContextValue + extends InfiniteTableStableContextValue { + state: InfiniteTableState; + computed: InfiniteTableComputedValues; children?: React.ReactNode; +} + +export interface InfiniteTableStableContextValue { api: InfiniteTableApi; dataSourceApi: DataSourceApi; - state: InfiniteTableState; + actions: InfiniteTableActions; dataSourceActions: DataSourceComponentActions; - computed: InfiniteTableComputedValues; getComputed: () => InfiniteTableComputedValues; getState: () => InfiniteTableState; getDataSourceState: () => DataSourceState; diff --git a/source/src/components/InfiniteTable/utils/getCellContextMenu.tsx b/source/src/components/InfiniteTable/utils/getCellContextMenu.tsx index 035fc472b..291423dca 100644 --- a/source/src/components/InfiniteTable/utils/getCellContextMenu.tsx +++ b/source/src/components/InfiniteTable/utils/getCellContextMenu.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Menu } from '../../Menu'; import { getCellContext } from '../components/InfiniteTableRow/columnRendering'; -import type { InfiniteTableContextValue } from '../types'; import { CellContextMenuLocationWithEvent, @@ -13,10 +12,11 @@ import { getMenuDefaultProps, getMenuItemsAndColumns, } from './contextMenuUtils'; +import { InfiniteTableStableContextValue } from '../types/InfiniteTableContextValue'; export function getCellContextMenu( cellLocation: CellContextMenuLocationWithEvent, - context: InfiniteTableContextValue, + context: InfiniteTableStableContextValue, onHideIntent?: VoidFunction, ) { const { columnId, rowIndex, event } = cellLocation; @@ -83,7 +83,7 @@ export function getCellContextMenu( export function getTableContextMenu( menuLocation: ContextMenuLocationWithEvent, - context: InfiniteTableContextValue, + context: InfiniteTableStableContextValue, onHideIntent?: VoidFunction, ) { const { getState, getComputed } = context; diff --git a/source/src/components/InfiniteTable/utils/getFilterOperatorMenuForColumn.tsx b/source/src/components/InfiniteTable/utils/getFilterOperatorMenuForColumn.tsx index 808c9aebb..d697d23e5 100644 --- a/source/src/components/InfiniteTable/utils/getFilterOperatorMenuForColumn.tsx +++ b/source/src/components/InfiniteTable/utils/getFilterOperatorMenuForColumn.tsx @@ -7,11 +7,11 @@ import { ClearIcon } from '../components/icons/ClearIcon'; import { DoneIcon } from '../components/icons/DoneIcon'; import { FilterIcon } from '../components/icons/FilterIcon'; -import { InfiniteTableContextValue } from '../types'; +import { InfiniteTableStableContextValue } from '../types/InfiniteTableContextValue'; export function getFilterOperatorMenuForColumn( columnId: string | null, - context: InfiniteTableContextValue, + context: InfiniteTableStableContextValue, onHideIntent?: VoidFunction, ) { if (columnId == null) { diff --git a/source/src/components/InfiniteTable/utils/getMenuForColumn.tsx b/source/src/components/InfiniteTable/utils/getMenuForColumn.tsx index 9606ee435..2d6e46424 100644 --- a/source/src/components/InfiniteTable/utils/getMenuForColumn.tsx +++ b/source/src/components/InfiniteTable/utils/getMenuForColumn.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import { Menu } from '../../Menu'; import { MenuState } from '../../Menu/MenuState'; import { getColumnApiForColumn } from '../api/getColumnApi'; -import { InfiniteTableContextValue } from '../types'; import { defaultGetColumnMenuItems } from './defaultGetColumnMenuItems'; +import { InfiniteTableStableContextValue } from '../types/InfiniteTableContextValue'; export function getMenuForColumn( columnId: string | null, - context: InfiniteTableContextValue, + context: InfiniteTableStableContextValue, onHideIntent?: VoidFunction, ) { if (columnId == null) { diff --git a/source/src/utils/ComponentStore.ts b/source/src/utils/ComponentStore.ts new file mode 100644 index 000000000..4b29f1587 --- /dev/null +++ b/source/src/utils/ComponentStore.ts @@ -0,0 +1,131 @@ +import { useCallback, useRef, useSyncExternalStore } from 'react'; +import { shallowEqualObjects } from './shallowEqualObjects'; + +export interface ComponentStore { + /** + * Returns the current context value snapshot. + */ + getSnapshot(): T; + + /** + * Updates the stored snapshot (called during provider render). + * Does NOT notify subscribers — call notify() separately after render. + */ + setSnapshot(value: T): void; + + /** + * Registers a listener that will be called on notify(). + * Returns an unsubscribe function. + */ + subscribe(listener: () => void): () => void; + + /** + * Notifies all subscribers that the snapshot may have changed. + * Should be called from useEffect (after commit) to avoid + * "Cannot update a component while rendering a different component" warnings. + */ + notify(): void; +} + +export function createComponentStore(): ComponentStore { + let snapshot: T = null as any; + const listeners = new Set<() => void>(); + + return { + getSnapshot() { + return snapshot; + }, + + setSnapshot(value: T) { + snapshot = value; + }, + + subscribe(listener: () => void) { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, + + notify() { + for (const listener of listeners) { + listener(); + } + }, + }; +} + +function useComponentStoreSelectorImpl( + store: ComponentStore, + selector: (ctx: CTX) => R, + isEqual: (a: R, b: R) => boolean, +): R { + // Use refs so callers don't need to memoize selector/isEqual + const selectorRef = useRef(selector); + selectorRef.current = selector; + + const isEqualRef = useRef(isEqual); + isEqualRef.current = isEqual; + + // Cache the previous result for referential stability + const resultRef = useRef(undefined as any); + const initializedRef = useRef(false); + + const getSnapshot = useCallback(() => { + const snapshot = store.getSnapshot(); + const nextResult = selectorRef.current(snapshot); + + if ( + initializedRef.current && + isEqualRef.current(resultRef.current as R, nextResult) + ) { + return resultRef.current as R; + } + + resultRef.current = nextResult; + initializedRef.current = true; + return nextResult; + }, [store]); + + return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot); +} + +/** + * A selector hook that reads a single value from the component store. + * Uses Object.is for equality — ideal for primitives and stable references. + * + * Components using this hook only re-render when the selected value + * actually changes, unlike useContext which re-renders on every context update. + * + * @param selector Function that extracts a value from the context. + * Does NOT need to be memoized by the caller. + */ +export function useComponentStoreSingleValue( + componentStore: ComponentStore, + selector: (ctx: CTX) => R, +): R { + return useComponentStoreSelectorImpl(componentStore, selector, Object.is); +} + +/** + * A selector hook that reads an object from the component store. + * Uses shallow equality — re-renders only when a property of the + * returned object actually changes (by reference). + * + * Use this when the selector returns a derived object (e.g. picking + * multiple properties), so that a new object literal with the same + * values does not trigger a re-render. + * + * @param selector Function that extracts an object from the context. + * Does NOT need to be memoized by the caller. + */ +export function useComponentStoreSelector( + componentStore: ComponentStore, + selector: (ctx: CTX) => R, +): R { + return useComponentStoreSelectorImpl( + componentStore, + selector, + shallowEqualObjects as (a: R, b: R) => boolean, + ); +} diff --git a/source/src/utils/groupAndPivot/index.ts b/source/src/utils/groupAndPivot/index.ts index cf8f2a33e..514ba3fa5 100644 --- a/source/src/utils/groupAndPivot/index.ts +++ b/source/src/utils/groupAndPivot/index.ts @@ -145,6 +145,7 @@ export type InfiniteTableRowInfoDataDiscriminator_RowInfoNormal = { isGroupRow: false; isTreeNode: false; isParentNode: false; + nodeExpanded: false; rowActive: boolean; rowDetailState: false | 'expanded' | 'collapsed'; rowInfo: @@ -194,6 +195,7 @@ export type InfiniteTableRowInfoDataDiscriminator_RowInfoGroup = { isGroupRow: true; isTreeNode: false; isParentNode: false; + nodeExpanded: false; field?: keyof T; value: any; rawValue: any; diff --git a/www/content/blog/2025/10/09/customizing-structure.page.tsx b/www/content/blog/2025/10/09/customizing-structure.page.tsx index f1e508101..a541ba1ba 100644 --- a/www/content/blog/2025/10/09/customizing-structure.page.tsx +++ b/www/content/blog/2025/10/09/customizing-structure.page.tsx @@ -5,6 +5,7 @@ import { DataSourceGroupBy, components, useDataSourceState, + DataSourceState, } from '@infinite-table/infinite-react'; import * as React from 'react'; @@ -104,7 +105,9 @@ export default function App() { } function AppGrid() { - const { dataArray } = useDataSourceState(); + const dataLength = useDataSourceState( + (state: DataSourceState) => state.dataArray.length, + ); return (
@@ -117,7 +120,7 @@ function AppGrid() {
- Showing {dataArray.length} rows + Showing {dataLength} rows
diff --git a/www/content/docs/reference/hooks/index.page.md b/www/content/docs/reference/hooks/index.page.md index 41294c404..65a799d54 100644 --- a/www/content/docs/reference/hooks/index.page.md +++ b/www/content/docs/reference/hooks/index.page.md @@ -29,7 +29,7 @@ The details for each city shows a DataGrid with developers in that city. - + > You can use it in your app components that are nested inside the `` @@ -41,6 +41,18 @@ Using it gives you access to the underlying data that InfiniteTable is using. +Call this hook with a `selector` function, which accepts the current `DataSourceState` as the first parameter. + +```ts title="Example usage - selecting the length of the data array" + +const length = useDataSourceState(state => state.dataArray.length) + +``` + + + + + Please make sure you know what you're doing. This is intended only for advanced and complex use-cases. diff --git a/www/content/docs/reference/hooks/using-datasource-context.page.tsx b/www/content/docs/reference/hooks/using-datasource-context.page.tsx index 8fc18b9ae..df241588c 100644 --- a/www/content/docs/reference/hooks/using-datasource-context.page.tsx +++ b/www/content/docs/reference/hooks/using-datasource-context.page.tsx @@ -5,6 +5,7 @@ import { DataSource, useDataSourceState, type InfiniteTableColumn, + DataSourceState, } from '@infinite-table/infinite-react'; type Developer = { @@ -52,7 +53,9 @@ export default function App() { } function AppGrid() { - const { dataArray } = useDataSourceState(); + const dataLength = useDataSourceState( + (state: DataSourceState) => state.dataArray.length, + ); return (

Your DataGrid

- Displaying {dataArray.length} rows. Collapse/expand rows to see this - number change. + Displaying {dataLength} rows. Collapse/expand rows to see this number + change.

diff --git a/www/next-env.d.ts b/www/next-env.d.ts index 830fb594c..9edff1c7c 100644 --- a/www/next-env.d.ts +++ b/www/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -/// +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/www/package.json b/www/package.json index 6d5492d89..477336b23 100644 --- a/www/package.json +++ b/www/package.json @@ -10,7 +10,7 @@ "touch-slug-pages": "npm run touch-docs-slug-page && npm run touch-blog-slug-page", "touch-docs-slug-page": "touch ./src/app/\\(docs\\)/docs/[...docsPages]/page.tsx", "touch-blog-slug-page": "touch ./src/app/blog/[...blogpost]/page.tsx", - "next-dev": "rm -fr .next && next dev -p 3001", + "next-dev": "rm -fr .next && next dev -p 3001 --turbopack", "build": "npm run copy-license && npm run index-content && npm --prefix dataserver install && npm run generate-lib-dts && npm run update-css-vars-list && npm run next-build", "next-build": "next build --turbopack", "update-css-vars-list": "node build/create-css-vars-list.js",