-
Notifications
You must be signed in to change notification settings - Fork 427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve DataTable re-rendering performance #2993
Changes from all commits
1b99eb0
e21f5ae
6d4ca7d
60a51fe
7172226
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because |
||
); | ||
|
||
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. | ||
* | ||
|
@@ -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 | ||
|
@@ -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` | ||
|
@@ -587,6 +687,13 @@ class DataTable extends React.Component { | |
} | ||
}; | ||
|
||
// eslint-disable-next-line camelcase | ||
UNSAFE_componentWillUpdate(nextProps) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fixing an existing issue (not related to performance) where navigating cells via keyboard and pressing spacebar wasn't triggering the interactive element. |
||
if (this.props.items !== nextProps.items) { | ||
this.interactiveElements = {}; | ||
} | ||
} | ||
|
||
isResizable() { | ||
return this.props.fixedLayout && this.props.resizable; | ||
} | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This essentially propagates all table props onto columns. In the extracted function I replaced this with individual props that are actually used. Passing more props than necessary can exacerbate the issue with unnecessary re-renders. |
||
// 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When extracting this code, I removed this property since |
||
}); | ||
} 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'; | ||
|
@@ -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( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with using
lodash.memoize
is it only bases its memoization on the first argument supplied to the function by default, and you need to hand-write the cache key calculation otherwise.memoize-one
is free of that shortcoming and is a popular library that's lightweight and performant.I think there is only one usage of
lodash.memoize
that I could remove if necessary.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, one memoize library would be great!