Skip to content

Commit 4546fb5

Browse files
authored
Merge pull request #732 from yergom/feature/rerender-improvements
✨Performance up! Reduce re-renders
2 parents 98475a9 + 68d39cc commit 4546fb5

21 files changed

+290
-212
lines changed

app/examples/column-properties-and-styling/ColumnFooterExample.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ import { employees } from '~/data';
88

99
const records = employees.slice(0, 10);
1010

11-
export function ColumnFooterExample() {
11+
export function ColumnFooterExample({ height }: { height?: number }) {
1212
return (
1313
// example-start
1414
<DataTable
1515
withTableBorder
1616
withColumnBorders
1717
striped
1818
records={records}
19+
height={height}
20+
withRowBorders
1921
columns={[
2022
{
2123
accessor: 'name',

app/examples/column-properties-and-styling/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ export default async function ColumnPropertiesAndStylingExamplePage() {
134134
<ColumnFooterExample />
135135
<Txt>Here’s the code for the example above:</Txt>
136136
<CodeBlock tabs={{ code, keys: ['ColumnFooterExample.tsx', 'data/index.ts'] }} />
137+
<Txt>The footer is always visible and sticks at the bottom. For example, if the table is scrollable:</Txt>
138+
<ColumnFooterExample height={200} />
139+
<Txt>Or if the table is higher than the amount of data:</Txt>
140+
<ColumnFooterExample height={550} />
137141
<PageSubtitle value="Styling column titles, cells and footers" />
138142
<Txt>In addition, each column can be further customized by specifying the following styling properties:</Txt>
139143
<UnorderedList>

package/DataTable.css

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
linear-gradient(to right, rgba(0, 0, 0, light-dark(0.05, 0.25)), rgba(0, 0, 0, 0)),
3838
linear-gradient(to right, rgba(0, 0, 0, light-dark(0.05, 0.25)), rgba(0, 0, 0, 0) 30%);
3939

40+
--mantine-datatable-header-height: 0;
41+
--mantine-datatable-footer-height: 0;
42+
--mantine-datatable-selection-column-width: 0;
43+
--mantine-datatable-top-shadow-opacity: 0;
44+
--mantine-datatable-left-shadow-opacity: 0;
45+
--mantine-datatable-bottom-shadow-opacity: 0;
46+
--mantine-datatable-right-shadow-opacity: 0;
47+
--mantine-datatable-footer-position: sticky;
48+
--mantine-datatable-footer-bottom: 0;
49+
--mantine-datatable-last-row-border-bottom: unset;
50+
4051
position: relative;
4152
display: flex;
4253
flex-direction: column;
@@ -91,6 +102,10 @@
91102
tbody tr:last-of-type {
92103
border-bottom: 0;
93104
}
105+
106+
tr:last-of-type:not(.mantine-datatable-empty-row) td {
107+
border-bottom: var(--mantine-datatable-last-row-border-bottom);
108+
}
94109
}
95110

96111
.mantine-datatable-vertical-align-top td {
@@ -127,7 +142,7 @@
127142
width: var(--mantine-spacing-xs);
128143
background: var(--mantine-datatable-shadow-background-right);
129144
pointer-events: none;
130-
opacity: 0;
145+
opacity: var(--mantine-datatable-right-shadow-opacity);
131146
transition: opacity 0.2s;
132147
}
133148
}
@@ -156,15 +171,6 @@
156171
background: var(--mantine-datatable-selection-color);
157172
}
158173
}
159-
160-
&-scrolled {
161-
th:last-of-type,
162-
td:not(.mantine-datatable-row-expansion-cell):last-of-type {
163-
&::after {
164-
opacity: 1;
165-
}
166-
}
167-
}
168174
}
169175

170176
.mantine-datatable-pin-first-column {
@@ -188,7 +194,7 @@
188194
width: var(--mantine-spacing-xs);
189195
background: var(--mantine-datatable-shadow-background-left);
190196
pointer-events: none;
191-
opacity: 0;
197+
opacity: var(--mantine-datatable-left-shadow-opacity);
192198
transition: opacity 0.2s;
193199
}
194200
}
@@ -235,16 +241,4 @@
235241
background: var(--mantine-datatable-selection-color);
236242
}
237243
}
238-
239-
&-scrolled:not(.mantine-datatable-selection-column-visible) th:first-of-type,
240-
&-scrolled:not(.mantine-datatable-selection-column-visible)
241-
td:not(.mantine-datatable-row-expansion-cell):first-of-type,
242-
&-scrolled.mantine-datatable-selection-column-visible th:first-of-type,
243-
&-scrolled.mantine-datatable-selection-column-visible tr:not(:nth-of-type(2)) th:nth-of-type(2),
244-
&-scrolled.mantine-datatable-selection-column-visible th.mantine-datatable-column-group-header-cell:nth-of-type(2),
245-
&-scrolled.mantine-datatable-selection-column-visible td:not(.mantine-datatable-row-expansion-cell):nth-of-type(2) {
246-
&::after {
247-
opacity: 1;
248-
}
249-
}
250244
}

package/DataTable.tsx

Lines changed: 23 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, Table, type MantineSize } from '@mantine/core';
2-
import { useDebouncedCallback, useMergedRef } from '@mantine/hooks';
2+
import { useMergedRef } from '@mantine/hooks';
33
import clsx from 'clsx';
4-
import { useCallback, useMemo, useState } from 'react';
4+
import { useCallback, useMemo } from 'react';
55
import { DataTableColumnsProvider } from './DataTableDragToggleProvider';
66
import { DataTableEmptyRow } from './DataTableEmptyRow';
77
import { DataTableEmptyState } from './DataTableEmptyState';
@@ -14,8 +14,7 @@ import { DataTableScrollArea } from './DataTableScrollArea';
1414
import { getTableCssVariables } from './cssVariables';
1515
import {
1616
useDataTableColumns,
17-
useElementOuterSize,
18-
useIsomorphicLayoutEffect,
17+
useDataTableInjectCssVariables,
1918
useLastSelectionChangeIndex,
2019
useRowExpansion,
2120
} from './hooks';
@@ -131,12 +130,6 @@ export function DataTable<T>({
131130
tableWrapper,
132131
...otherProps
133132
}: DataTableProps<T>) {
134-
const {
135-
ref: localScrollViewportRef,
136-
width: scrollViewportWidth,
137-
height: scrollViewportHeight,
138-
} = useElementOuterSize<HTMLDivElement>();
139-
140133
const effectiveColumns = useMemo(() => {
141134
return groups?.flatMap((group) => group.columns) ?? columns!;
142135
}, [columns, groups]);
@@ -150,84 +143,28 @@ export function DataTable<T>({
150143
columns: effectiveColumns,
151144
});
152145

153-
const { ref: headerRef, height: headerHeight } = useElementOuterSize<HTMLTableSectionElement>();
154-
const { ref: localTableRef, width: tableWidth, height: tableHeight } = useElementOuterSize<HTMLTableElement>();
155-
const { ref: footerRef, height: footerHeight } = useElementOuterSize<HTMLTableSectionElement>();
156-
const { ref: paginationRef, height: paginationHeight } = useElementOuterSize<HTMLDivElement>();
157-
const { ref: selectionColumnHeaderRef, width: selectionColumnWidth } = useElementOuterSize<HTMLTableCellElement>();
158-
159-
const mergedTableRef = useMergedRef(localTableRef, tableRef);
160-
const mergedViewportRef = useMergedRef(localScrollViewportRef, scrollViewportRef);
146+
const { refs, onScroll: handleScrollPositionChange } = useDataTableInjectCssVariables({
147+
scrollCallbacks: {
148+
onScroll,
149+
onScrollToTop,
150+
onScrollToBottom,
151+
onScrollToLeft,
152+
onScrollToRight,
153+
},
154+
withRowBorders: otherProps.withRowBorders,
155+
});
161156

162-
const [scrolledToTop, setScrolledToTop] = useState(true);
163-
const [scrolledToBottom, setScrolledToBottom] = useState(true);
164-
const [scrolledToLeft, setScrolledToLeft] = useState(true);
165-
const [scrolledToRight, setScrolledToRight] = useState(true);
157+
const mergedTableRef = useMergedRef(refs.table, tableRef);
158+
const mergedViewportRef = useMergedRef(refs.scrollViewport, scrollViewportRef);
166159

167160
const rowExpansionInfo = useRowExpansion<T>({ rowExpansion, records, idAccessor });
168161

169-
const processScrolling = useCallback(() => {
170-
const scrollTop = localScrollViewportRef.current?.scrollTop ?? 0;
171-
const scrollLeft = localScrollViewportRef.current?.scrollLeft ?? 0;
172-
173-
if (fetching || tableHeight <= scrollViewportHeight) {
174-
setScrolledToTop(true);
175-
setScrolledToBottom(true);
176-
} else {
177-
const newScrolledToTop = scrollTop === 0;
178-
const newScrolledToBottom = tableHeight - scrollTop - scrollViewportHeight < 1;
179-
setScrolledToTop(newScrolledToTop);
180-
setScrolledToBottom(newScrolledToBottom);
181-
if (newScrolledToTop && newScrolledToTop !== scrolledToTop) onScrollToTop?.();
182-
if (newScrolledToBottom && newScrolledToBottom !== scrolledToBottom) onScrollToBottom?.();
183-
}
184-
185-
if (fetching || tableWidth === scrollViewportWidth) {
186-
setScrolledToLeft(true);
187-
setScrolledToRight(true);
188-
} else {
189-
const newScrolledToLeft = scrollLeft === 0;
190-
const newScrolledToRight = tableWidth - scrollLeft - scrollViewportWidth < 1;
191-
setScrolledToLeft(newScrolledToLeft);
192-
setScrolledToRight(newScrolledToRight);
193-
if (newScrolledToLeft && newScrolledToLeft !== scrolledToLeft) onScrollToLeft?.();
194-
if (newScrolledToRight && newScrolledToRight !== scrolledToRight) onScrollToRight?.();
195-
}
196-
}, [
197-
fetching,
198-
onScrollToBottom,
199-
onScrollToLeft,
200-
onScrollToRight,
201-
onScrollToTop,
202-
scrollViewportHeight,
203-
localScrollViewportRef,
204-
scrollViewportWidth,
205-
scrolledToBottom,
206-
scrolledToLeft,
207-
scrolledToRight,
208-
scrolledToTop,
209-
tableHeight,
210-
tableWidth,
211-
]);
212-
213-
useIsomorphicLayoutEffect(processScrolling, [processScrolling]);
214-
215-
const debouncedProcessScrolling = useDebouncedCallback(processScrolling, 50);
216-
217-
const handleScrollPositionChange = useCallback(
218-
(e: { x: number; y: number }) => {
219-
onScroll?.(e);
220-
debouncedProcessScrolling();
221-
},
222-
[debouncedProcessScrolling, onScroll]
223-
);
224-
225162
const handlePageChange = useCallback(
226163
(page: number) => {
227-
localScrollViewportRef.current?.scrollTo({ top: 0, left: 0 });
164+
refs.scrollViewport.current?.scrollTo({ top: 0, left: 0 });
228165
onPageChange!(page);
229166
},
230-
[onPageChange, localScrollViewportRef]
167+
[onPageChange, refs.scrollViewport]
231168
);
232169

233170
const recordsLength = records?.length;
@@ -263,7 +200,7 @@ export function DataTable<T>({
263200
]);
264201

265202
const { lastSelectionChangeIndex, setLastSelectionChangeIndex } = useLastSelectionChangeIndex(recordIds);
266-
const selectorCellShadowVisible = selectionColumnVisible && !scrolledToLeft && !pinFirstColumn;
203+
const selectorCellShadowVisible = selectionColumnVisible && !pinFirstColumn;
267204

268205
const marginProperties = { m, my, mx, mt, mb, ml, mr };
269206

@@ -278,6 +215,7 @@ export function DataTable<T>({
278215
return (
279216
<DataTableColumnsProvider {...dragToggle}>
280217
<Box
218+
ref={refs.root}
281219
{...marginProperties}
282220
className={clsx(
283221
'mantine-datatable',
@@ -311,14 +249,8 @@ export function DataTable<T>({
311249
>
312250
<DataTableScrollArea
313251
viewportRef={mergedViewportRef}
314-
topShadowVisible={!scrolledToTop}
315-
leftShadowVisible={!scrolledToLeft}
316252
leftShadowBehind={selectionColumnVisible || !!pinFirstColumn}
317-
rightShadowVisible={!scrolledToRight}
318253
rightShadowBehind={pinLastColumn}
319-
bottomShadowVisible={!scrolledToBottom}
320-
headerHeight={headerHeight}
321-
footerHeight={footerHeight}
322254
onScrollPositionChange={handleScrollPositionChange}
323255
scrollAreaProps={scrollAreaProps}
324256
>
@@ -332,20 +264,15 @@ export function DataTable<T>({
332264
[TEXT_SELECTION_DISABLED]: textSelectionDisabled,
333265
'mantine-datatable-vertical-align-top': verticalAlign === 'top',
334266
'mantine-datatable-vertical-align-bottom': verticalAlign === 'bottom',
335-
'mantine-datatable-last-row-border-bottom-visible':
336-
otherProps.withRowBorders && tableHeight < scrollViewportHeight,
337267
'mantine-datatable-pin-last-column': pinLastColumn,
338-
'mantine-datatable-pin-last-column-scrolled': !scrolledToRight && pinLastColumn,
339268
'mantine-datatable-selection-column-visible': selectionColumnVisible,
340269
'mantine-datatable-pin-first-column': pinFirstColumn,
341-
'mantine-datatable-pin-first-column-scrolled': !scrolledToLeft && pinFirstColumn,
342270
'mantine-datatable-resizable-columns': hasResizableColumns,
343271
},
344272
classNames?.table
345273
)}
346274
style={{
347275
...styles?.table,
348-
'--mantine-datatable-selection-column-width': `${selectionColumnWidth}px`,
349276
}}
350277
data-striped={(recordsLength && striped) || undefined}
351278
data-highlight-on-hover={highlightOnHover || undefined}
@@ -354,8 +281,8 @@ export function DataTable<T>({
354281
{noHeader ? null : (
355282
<DataTableColumnsProvider {...dragToggle}>
356283
<DataTableHeader<T>
357-
ref={headerRef}
358-
selectionColumnHeaderRef={selectionColumnHeaderRef}
284+
ref={refs.header}
285+
selectionColumnHeaderRef={refs.selectionColumnHeader}
359286
className={classNames?.header}
360287
style={styles?.header}
361288
columns={effectiveColumns}
@@ -456,14 +383,13 @@ export function DataTable<T>({
456383

457384
{effectiveColumns.some(({ footer }) => footer) && (
458385
<DataTableFooter<T>
459-
ref={footerRef}
386+
ref={refs.footer}
460387
className={classNames?.footer}
461388
style={styles?.footer}
462389
columns={effectiveColumns}
463390
defaultColumnProps={defaultColumnProps}
464391
selectionVisible={selectionColumnVisible}
465392
selectorCellShadowVisible={selectorCellShadowVisible}
466-
scrollDiff={tableHeight - scrollViewportHeight}
467393
/>
468394
)}
469395
</Table>
@@ -472,7 +398,6 @@ export function DataTable<T>({
472398

473399
{page && (
474400
<DataTablePagination
475-
ref={paginationRef}
476401
className={classNames?.pagination}
477402
style={styles?.pagination}
478403
horizontalSpacing={horizontalSpacing}
@@ -498,22 +423,14 @@ export function DataTable<T>({
498423
/>
499424
)}
500425
<DataTableLoader
501-
pt={headerHeight}
502-
pb={paginationHeight}
503426
fetching={fetching}
504427
backgroundBlur={loaderBackgroundBlur}
505428
customContent={customLoader}
506429
size={loaderSize}
507430
type={loaderType}
508431
color={loaderColor}
509432
/>
510-
<DataTableEmptyState
511-
pt={headerHeight}
512-
pb={paginationHeight}
513-
icon={noRecordsIcon}
514-
text={noRecordsText}
515-
active={!fetching && !recordsLength}
516-
>
433+
<DataTableEmptyState icon={noRecordsIcon} text={noRecordsText} active={!fetching && !recordsLength}>
517434
{emptyState}
518435
</DataTableEmptyState>
519436
</Box>

package/DataTableEmptyState.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-3));
1010
opacity: 0;
1111
transition: opacity 0.2s;
12+
padding-top: var(--mantine-datatable-header-height, 0);
13+
padding-bottom: var(--mantine-datatable-footer-height, 0);
1214

1315
&[data-active] {
1416
opacity: 1;

package/DataTableEmptyState.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import { Center, Text, type MantineSpacing, type StyleProp } from '@mantine/core';
1+
import { Center, Text } from '@mantine/core';
22
import { IconDatabaseOff } from './icons/IconDatabaseOff';
33

44
type DataTableEmptyStateProps = React.PropsWithChildren<{
55
icon: React.ReactNode | undefined;
66
text: string;
7-
pt: StyleProp<MantineSpacing>;
8-
pb: StyleProp<MantineSpacing>;
97
active: boolean;
108
}>;
119

12-
export function DataTableEmptyState({ icon, text, pt, pb, active, children }: DataTableEmptyStateProps) {
10+
export function DataTableEmptyState({ icon, text, active, children }: DataTableEmptyStateProps) {
1311
return (
14-
<Center pt={pt} pb={pb} className="mantine-datatable-empty-state" data-active={active || undefined}>
12+
<Center className="mantine-datatable-empty-state" data-active={active || undefined}>
1513
{children || (
1614
<>
1715
{icon || (

package/DataTableFooter.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.mantine-datatable-footer {
22
z-index: 2;
3-
3+
position: var(--mantine-datatable-footer-position);
4+
bottom: var(--mantine-datatable-footer-bottom);
45
th {
56
border-top: rem(1px) solid var(--mantine-datatable-border-color);
67
}

0 commit comments

Comments
 (0)