Skip to content

Commit

Permalink
Improve DataTable re-rendering performance (#2993)
Browse files Browse the repository at this point in the history
* Improve DataTable rerendering performance

* Remove lodash.memoize

Co-authored-by: Stephen James <sjames@salesforce.com>
  • Loading branch information
Alex Zherdev and interactivellama committed Feb 11, 2022
1 parent bdefc03 commit 9a384ab
Show file tree
Hide file tree
Showing 12 changed files with 821 additions and 719 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
'json',
'prefer-object-spread',
'@salesforce/slds-react',
'react-hooks',
],
env: {
browser: true,
Expand Down Expand Up @@ -195,6 +196,9 @@ module.exports = {
// 'fp/no-unused-expression': 'error',
'fp/no-valueof-field': 'error',

'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',

//
// THE FOLLOWING RULES NEED REVIEW IN THE FUTURE (and possibly removed)
//
Expand Down
1 change: 0 additions & 1 deletion components/button/__tests__/button.browser-test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('SLDSButton: ', () => {
style: { background: 'rgb(18, 49, 35)' },
});
[btn] = cmp.getElementsByClassName('slds-button');
console.log('!!!!!', cmp, btn);
});

it('renders correct label', () => {
Expand Down
4 changes: 2 additions & 2 deletions components/data-table/cell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import classNames from 'classnames';

import CellContext from './private/cell-context';
import TableContext from './private/table-context';
import contextHelper from './private/context-helper';
import useContextHelper from './private/context-helper';

// ## Constants
import { DATA_TABLE_CELL } from '../../utilities/constants';
Expand All @@ -21,7 +21,7 @@ import { DATA_TABLE_CELL } from '../../utilities/constants';
const DataTableCell = (props) => {
const tableContext = useContext(TableContext);
const cellContext = useContext(CellContext);
const { tabIndex, hasFocus, handleFocus, handleKeyDown } = contextHelper(
const { tabIndex, hasFocus, handleFocus, handleKeyDown } = useContextHelper(
tableContext,
cellContext,
props.fixedLayout
Expand Down
215 changes: 130 additions & 85 deletions components/data-table/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import shortid from 'shortid';

import classNames from 'classnames';
import assign from 'lodash.assign';
import isEqual from 'lodash.isequal';
import memoize from 'memoize-one';
import reject from 'lodash.reject';

// This component's `checkProps` which issues warnings to developers about properties when in development mode (similar to React's built in development tools)
import ColumnResizer from 'column-resizer';
import checkProps from './check-props';
Expand Down Expand Up @@ -74,6 +75,90 @@ const defaultProps = {
},
};

const getAssistiveText = memoize(
(
assistiveText,
actionsHeaderText,
columnSortText,
columnSortedAscendingText,
columnSortedDescendingText,
selectAllRowsText,
selectRowText
) => {
const result = {
...defaultProps.assistiveText,
...assistiveText,
};
if (actionsHeaderText) {
result.actionsHeader = actionsHeaderText;
}
if (selectAllRowsText) {
result.selectAllRows = selectAllRowsText;
}
if (columnSortedAscendingText) {
result.columnSortedAscending = columnSortedAscendingText;
}
if (columnSortedDescendingText) {
result.columnSortedDescending = columnSortedDescendingText;
}
if (columnSortText) {
result.columnSort = columnSortText;
}
if (selectRowText) {
result.selectRow = selectRowText;
}
return result;
},
isEqual
);

const getColumnsAndRowActions = memoize(
(children, id, fixedHeader, fixedLayout, search) => {
const columns = [];
let RowActions = null;

React.Children.forEach(children, (child) => {
if (child && child.type.displayName === DataTableColumn.displayName) {
const { children: columnChildren, ...columnProps } = child.props;
const props = { fixedLayout, search, id, ...columnProps };

let Cell;
if (
columnChildren &&
columnChildren.type.displayName === DATA_TABLE_CELL
) {
Cell = columnChildren.type;
assign(props, columnChildren.props);
} else {
Cell = DataTableCell;
}

// eslint-disable-next-line fp/no-mutating-methods
columns.push({
Cell,
props,
});
} else if (
child &&
child.type.displayName === DataTableRowActions.displayName
) {
const { dropdown } = child.props;
const dropdownPropOverrides = {};
if (fixedHeader) {
dropdownPropOverrides.menuPosition = 'overflowBoundaryElement';
}
RowActions = React.cloneElement(child, {
dropdown: dropdown
? React.cloneElement(dropdown, dropdownPropOverrides)
: null,
});
}
});
return { columns, RowActions };
},
isEqual
);

/**
* DataTables support the display of structured data in rows and columns with an HTML table. To sort, filter or paginate the table, simply update the data passed in the items to the table and it will re-render itself appropriately. The table will throw a sort event as needed, and helper components for paging and filtering are coming soon.
*
Expand Down Expand Up @@ -366,9 +451,7 @@ class DataTable extends React.Component {
// Simulating a scroll here will ensure that enough rows are loaded to enable scrolling
this.loadMoreIfNeeded();
}
if (this.props.items !== prevProps.items) {
this.interactiveElements = {};
}

if (
this.state.allowKeyboardNavigation &&
!prevState.allowKeyboardNavigation
Expand Down Expand Up @@ -440,6 +523,23 @@ class DataTable extends React.Component {
return null;
}

getTableContext = memoize((state, isKeyboardNavigation) => ({
activeCell: state.activeCell,
activeElement: state.activeElement,
mode: state.mode,
tableHasFocus: state.tableHasFocus,
changeActiveCell: this.changeActiveCell,
changeActiveElement: this.changeActiveElement,
handleKeyDown: this.handleKeyDown,
registerInteractiveElement: this.registerInteractiveElement,
allowKeyboardNavigation: state.allowKeyboardNavigation,
setAllowKeyboardNavigation: (allowKeyboardNavigation) => {
if (isKeyboardNavigation) {
this.setState({ allowKeyboardNavigation });
}
},
}));

handleToggleAll = (e, { checked }) => {
// REMOVE AT NEXT BREAKING CHANGE
// `onChange` is deprecated and replaced with `onRowChange`
Expand Down Expand Up @@ -587,6 +687,13 @@ class DataTable extends React.Component {
}
};

// eslint-disable-next-line camelcase
UNSAFE_componentWillUpdate(nextProps) {
if (this.props.items !== nextProps.items) {
this.interactiveElements = {};
}
}

isResizable() {
return this.props.fixedLayout && this.props.resizable;
}
Expand Down Expand Up @@ -989,71 +1096,24 @@ class DataTable extends React.Component {
const allSelected = canSelectRows && numNonHeaderRows === numSelected;
const indeterminateSelected =
canSelectRows && numNonHeaderRows !== numSelected && numSelected !== 0;
const columns = [];
let RowActions = null;

React.Children.forEach(this.props.children, (child) => {
if (child && child.type.displayName === DataTableColumn.displayName) {
const { children, ...columnProps } = child.props;

const props = assign({}, this.props);
// eslint-disable-next-line fp/no-delete
delete props.children;
assign(props, columnProps);

let Cell;
if (children && children.type.displayName === DATA_TABLE_CELL) {
Cell = children.type;
assign(props, children.props);
} else {
Cell = DataTableCell;
}

// eslint-disable-next-line fp/no-mutating-methods
columns.push({
Cell,
props,
dataTableProps: this.props,
});
} else if (
child &&
child.type.displayName === DataTableRowActions.displayName
) {
const { dropdown } = child.props;
const dropdownPropOverrides = {};
if (this.getFixedHeader()) {
dropdownPropOverrides.menuPosition = 'overflowBoundaryElement';
}
RowActions = React.cloneElement(child, {
dropdown: dropdown
? React.cloneElement(dropdown, dropdownPropOverrides)
: null,
});
}
});
const { columns, RowActions } = getColumnsAndRowActions(
this.props.children,
this.props.id,
this.getFixedHeader(),
this.props.fixedLayout,
this.props.search
);

const assistiveText = {
...defaultProps.assistiveText,
...this.props.assistiveText,
};
if (this.props.assistiveTextForActionsHeader) {
assistiveText.actionsHeader = this.props.assistiveTextForActionsHeader;
}
if (this.props.assistiveTextForSelectAllRows) {
assistiveText.selectAllRows = this.props.assistiveTextForSelectAllRows;
}
if (this.props.assistiveTextForColumnSortedAscending) {
assistiveText.columnSortedAscending = this.props.assistiveTextForColumnSortedAscending;
}
if (this.props.assistiveTextForColumnSortedDescending) {
assistiveText.columnSortedDescending = this.props.assistiveTextForColumnSortedDescending;
}
if (this.props.assistiveTextForColumnSort) {
assistiveText.columnSort = this.props.assistiveTextForColumnSort;
}
if (this.props.assistiveTextForSelectRow) {
assistiveText.selectRow = this.props.assistiveTextForSelectRow;
}
const assistiveText = getAssistiveText(
this.props.assistiveText,
this.props.assistiveTextForActionsHeader,
this.props.assistiveTextForSelectAllRows,
this.props.assistiveTextForColumnSortedAscending,
this.props.assistiveTextForColumnSortedDescending,
this.props.assistiveTextForColumnSort,
this.props.assistiveTextForSelectRow
);

if (this.props.selectRows && this.props.selectRows !== 'radio') {
ariaProps['aria-multiselectable'] = 'true';
Expand All @@ -1066,26 +1126,11 @@ class DataTable extends React.Component {
select: canSelectRows ? this.headerRefs.select : [],
};

const tableContext = {
activeCell: this.state.activeCell,
activeElement: this.state.activeElement,
mode: this.state.mode,
tableHasFocus: this.state.tableHasFocus,
changeActiveCell: this.changeActiveCell,
changeActiveElement: this.changeActiveElement,
handleKeyDown: this.handleKeyDown,
registerInteractiveElement: this.registerInteractiveElement,
allowKeyboardNavigation: this.state.allowKeyboardNavigation,
setAllowKeyboardNavigation: (allowKeyboardNavigation) => {
if (this.getKeyboardNavigation()) {
this.setState({ allowKeyboardNavigation });
}
},
};

let component = (
<React.Fragment>
<TableContext.Provider value={tableContext}>
<TableContext.Provider
value={this.getTableContext(this.state, this.getKeyboardNavigation())}
>
<table
{...ariaProps}
className={classNames(
Expand Down
40 changes: 26 additions & 14 deletions components/data-table/private/context-helper.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */

import { useCallback } from 'react';

/**
* Calculates data table keyboard navigation state based on currently selected cell
*/
export default (tableContext, cellContext, fixedLayout) => {
export default function useTableContextHelper(
tableContext,
cellContext,
fixedLayout
) {
const isActive =
tableContext.activeCell.rowIndex === cellContext.rowIndex &&
tableContext.activeCell.columnIndex === cellContext.columnIndex;

const hasFocus = fixedLayout && tableContext.tableHasFocus && isActive;

const handleFocus = () => {
const { changeActiveCell, handleKeyDown: handleTableKeyDown } = tableContext;
const handleFocus = useCallback(() => {
if (fixedLayout && tableContext.allowKeyboardNavigation) {
tableContext.changeActiveCell(
cellContext.rowIndex,
cellContext.columnIndex
);
changeActiveCell(cellContext.rowIndex, cellContext.columnIndex);
}
};
}, [
fixedLayout,
tableContext.allowKeyboardNavigation,
changeActiveCell,
cellContext.rowIndex,
cellContext.columnIndex,
]);

const handleKeyDown = (event) => {
if (fixedLayout && tableContext.allowKeyboardNavigation) {
tableContext.handleKeyDown(event);
}
};
const handleKeyDown = useCallback(
(event) => {
if (fixedLayout && tableContext.allowKeyboardNavigation) {
handleTableKeyDown(event);
}
},
[fixedLayout, tableContext.allowKeyboardNavigation, handleTableKeyDown]
);

const tabIndex =
fixedLayout &&
Expand All @@ -35,4 +47,4 @@ export default (tableContext, cellContext, fixedLayout) => {
: undefined;

return { tabIndex, hasFocus, handleFocus, handleKeyDown };
};
}

0 comments on commit 9a384ab

Please sign in to comment.