Refactor useTable, sorting, and filtering to use new hook layer

This commit is contained in:
tannerlinsley
2019-07-29 14:51:07 -06:00
parent 14a43b1a68
commit 11167e5635
10 changed files with 454 additions and 261 deletions

View File

@@ -33,9 +33,7 @@ Hooks for building **lightweight, fast and extendable datagrids** for React
- Extensible via hooks
- <a href="https://medium.com/@tannerlinsley/why-i-wrote-react-table-and-the-problems-it-has-solved-for-nozzle-others-445c4e93d4a8#.axza4ixba" target="\_parent">"Why I wrote React Table and the problems it has solved for Nozzle.io"</a> by Tanner Linsley
## Demos
[React Table v7 Sandbox](https://codesandbox.io/s/m5lxzzpz69)
## [See Examples](#examples)
## Versions
@@ -237,12 +235,15 @@ const instance = useTable(
1. `useTable` is called. A table instance is created.
1. The `instance.state` is resolved from either a custom user state or an automatically generated one.
1. A collection of plugin points is created at `instance.hooks`. These plugin points don't run until after all of the plugins have run.
1. The instance is reduced through each plugin hook in the order they were called. Each hook receives the result of the previous hook, is able to manipulate the `instance`, use plugin points, use their own React hooks internally and eventually return a new `instance`. This happens until the last instance object is returned from the last hook.
1. Lastly, the plugin points that were registered and populated during hook reduction are run to produce the final instance object that is returned from `useTable`
1. A collection of plugin points is created at `instance.hooks`.
1. Each plugin is given the opportunity to add hooks to `instance.hook`.
1. As the `useTable` logic proceeds to run, each plugin hook type is used at a specific point in time with each individual hook function being executed the order it was registered.
1. The final instance object is returned from `useTable`, which the developer then uses to construct their table.
This multi-stage process is the secret sauce that allows React Table plugin hooks to work together and compose nicely, while not stepping on each others toes.
To dive deeper into plugins, see [Plugins](TODO) and the [Plugin Guide](TODO)
### Plugin Hook Order & Consistency
The order and usage of plugin hooks must follow [The Laws of Hooks](TODO), just like any other custom hook. They must always be unconditionally called in the same order.
@@ -286,9 +287,6 @@ The following options are supported via the main options object passed to `useTa
- The default column object for every column passed to React Table.
- Column-specific properties will override the properties in this object, eg. `{ ...defaultColumn, ...userColumn }`
- This is particularly useful for adding global column properties. For instance, when using the `useFilters` plugin hook, add a default `Filter` renderer for every column, eg.`{ Filter: MyDefaultFilterComponent }`
- `useRows: Function`
- Optional
- This hook overrides the internal `useRows` hooks used by `useTable`. You probably never want to override this unless you are testing or developing new features for React Table
- `debug: Bool`
- Optional
- A flag to turn on debug mode.
@@ -580,7 +578,7 @@ The following values are provided to the table `instance`:
The following properties are available on every `Column` object returned by the table instance.
- `canSortBy: Bool`
- `canSort: Bool`
- Denotes whether a column is sortable or not depending on if it has a valid accessor/data model or is manually disabled via an option.
- `toggleSortBy: Function(descending, multi) => void`
- This function can be used to programmatically toggle the sorting for this column.

View File

@@ -33,47 +33,72 @@ const Styles = styled.div`
}
`
const defaultColumn = {
sort: 'numeric',
}
function Table({ columns, data }) {
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
{
columns,
data,
defaultColumn,
debug: true,
},
useSortBy
)
// We don't want to render all 2000 rows for this example, so cap
// it at 20 for this use case
const firstPageRows = rows.slice(0, 20)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted ? (column.sortedDesc ? ' 🔽' : ' 🔼') : ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
<>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(
column =>
console.log(column) || (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
>
{column.render('Header')}
{/* Add a sort direction indicator */}
<span>
{column.sorted
? column.sortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</th>
)
)}
</tr>
))}
</thead>
<tbody>
{firstPageRows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
)
)}
</tbody>
</table>
<br />
<div>Showing the first 20 results of {rows.length} rows</div>
</>
)
}
@@ -118,7 +143,7 @@ function App() {
[]
)
const data = React.useMemo(() => makeData(20), [])
const data = React.useMemo(() => makeData(1000), [])
return (
<Styles>

View File

@@ -89,6 +89,4 @@ export const between = (rows, id, filterValue) => {
}
between.autoRemove = val =>
console.log(val) ||
!val ||
(typeof val[0] !== 'number' && typeof val[1] !== 'number')
!val || (typeof val[0] !== 'number' && typeof val[1] !== 'number')

View File

@@ -0,0 +1,111 @@
import '@testing-library/react/cleanup-after-each'
import '@testing-library/jest-dom/extend-expect'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required
import React from 'react'
import { render } from '@testing-library/react'
import useTable from './useTable'
function Table({ columns, data }) {
// Use the state and functions returned from useTable to build your UI
const { getTableProps, headerGroups, rows, prepareRow } = useTable({
columns,
data,
})
// Render the UI for your table
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody>
{rows.map(
(row, i) =>
prepareRow(row) || (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
)}
</tbody>
</table>
)
}
function App() {
const columns = React.useMemo(
() => [
{
Header: 'Name',
columns: [
{
Header: 'First Name',
accessor: 'firstName',
},
{
Header: 'Last Name',
accessor: 'lastName',
},
],
},
{
Header: 'Info',
columns: [
{
Header: 'Age',
accessor: 'age',
},
{
Header: 'Visits',
accessor: 'visits',
},
{
Header: 'Status',
accessor: 'status',
},
{
Header: 'Profile Progress',
accessor: 'progress',
},
],
},
],
[]
)
const data = React.useMemo(
() => [
{
firstName: 'tanner',
lastName: 'linsley',
age: 29,
visits: 100,
status: 'In Relationship',
progress: 50,
},
],
[]
)
return <Table columns={columns} data={data} />
}
test('renders a basic table', () => {
const { getByText } = render(<App />)
expect(getByText('tanner')).toBeInTheDocument()
expect(getByText('linsley')).toBeInTheDocument()
expect(getByText('29')).toBeInTheDocument()
expect(getByText('100')).toBeInTheDocument()
expect(getByText('In Relationship')).toBeInTheDocument()
expect(getByText('50')).toBeInTheDocument()
})

View File

@@ -9,6 +9,7 @@ import {
decorateColumnTree,
makeHeaderGroups,
findMaxDepth,
flattenBy,
} from '../utils'
import { useTableState } from './useTableState'
@@ -52,34 +53,36 @@ export const useTable = (props, ...plugins) => {
// But use the users state if provided
const state = userState || defaultState
// These are hooks that plugins can use right before render
const hooks = {
beforeRender: [],
columns: [],
headers: [],
headerGroups: [],
rows: [],
row: [],
getTableProps: [],
getRowProps: [],
getHeaderGroupProps: [],
getHeaderProps: [],
getCellProps: [],
}
// The initial api
let api = {
let instanceRef = React.useRef({})
Object.assign(instanceRef.current, {
...props,
data,
state,
hooks,
plugins,
}
hooks: {
columnsBeforeHeaderGroups: [],
useMain: [],
useColumns: [],
useHeaders: [],
useHeaderGroups: [],
useRows: [],
prepareRow: [],
getTableProps: [],
getRowProps: [],
getHeaderGroupProps: [],
getHeaderProps: [],
getCellProps: [],
},
})
if (debug) console.time('hooks')
// Loop through plugins to build the api out
api = plugins.filter(Boolean).reduce((prev, next) => next(prev), api)
if (debug) console.timeEnd('hooks')
// Allow plugins to register hooks
if (debug) console.time('plugins')
plugins.filter(Boolean).forEach(plugin => {
plugin(instanceRef.current.hooks)
})
if (debug) console.timeEnd('plugins')
// Compute columns, headerGroups and headers
const columnInfo = React.useMemo(
@@ -93,7 +96,11 @@ export const useTable = (props, ...plugins) => {
// Allow hooks to decorate columns
if (debug) console.time('hooks.columnsBeforeHeaderGroups')
columns = applyHooks(api.hooks.columnsBeforeHeaderGroups, columns, api)
columns = applyHooks(
instanceRef.current.hooks.columnsBeforeHeaderGroups,
columns,
instanceRef.current
)
if (debug) console.timeEnd('hooks.columnsBeforeHeaderGroups')
// Make the headerGroups
@@ -111,17 +118,16 @@ export const useTable = (props, ...plugins) => {
headers,
}
},
[api, debug, defaultColumn, userColumns]
[debug, defaultColumn, userColumns]
)
// Place the columns, headerGroups and headers on the api
Object.assign(api, columnInfo)
Object.assign(instanceRef.current, columnInfo)
// Access the row model
api.rows = React.useMemo(
instanceRef.current.rows = React.useMemo(
() => {
if (debug) console.info('getAccessedRows')
if (debug) console.time('getAccessedRows')
// Access the row's data
const accessRow = (originalRow, i, depth = 0) => {
// Keep the original reference around
@@ -155,7 +161,7 @@ export const useTable = (props, ...plugins) => {
// Create the cells and values
row.values = {}
api.columns.forEach(column => {
instanceRef.current.columns.forEach(column => {
row.values[column.id] = column.accessor
? column.accessor(originalRow, i, { subRows, depth, data })
: undefined
@@ -165,61 +171,89 @@ export const useTable = (props, ...plugins) => {
}
// Use the resolved data
return data.map((d, i) => accessRow(d, i))
const accessedData = data.map((d, i) => accessRow(d, i))
if (debug) console.timeEnd('getAccessedRows')
return accessedData
},
[debug, data, subRowsKey, api.columns]
[debug, data, subRowsKey]
)
// Determine column visibility
api.columns.forEach(column => {
instanceRef.current.columns.forEach(column => {
column.visible =
typeof column.show === 'function' ? column.show(api) : !!column.show
typeof column.show === 'function'
? column.show(instanceRef.current)
: !!column.show
})
// Allow hooks to decorate columns
if (debug) console.time('hooks.columns')
api.columns = applyHooks(api.hooks.columns, api.columns, api)
if (debug) console.timeEnd('hooks.columns')
if (debug) console.time('hooks.useMain')
instanceRef.current = applyHooks(
instanceRef.current.hooks.useMain,
instanceRef.current
)
if (debug)
console.timeEnd('hooks.useMain')
// Allow hooks to decorate headers
if (debug) console.time('hooks.headers')
api.headers = applyHooks(api.hooks.headers, api.headers, api)
if (debug) console.timeEnd('hooks.headers')
;[...api.columns, ...api.headers].forEach(column => {
// Give columns/headers rendering power
column.render = (type, userProps = {}) => {
const Comp = typeof type === 'string' ? column[type] : type
// // Allow hooks to decorate columns
// if (debug) console.time('hooks.useColumns')
// instanceRef.current.columns = applyHooks(
// instanceRef.current.hooks.useColumns,
// instanceRef.current.columns,
// instanceRef.current
// )
// if (debug) console.timeEnd('hooks.useColumns')
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
// // Allow hooks to decorate headers
// if (debug) console.time('hooks.useHeaders')
// instanceRef.current.headers = applyHooks(
// instanceRef.current.hooks.useHeaders,
// instanceRef.current.headers,
// instanceRef.current
// )
// if (debug) console.timeEnd('hooks.useHeaders')
;[...instanceRef.current.columns, ...instanceRef.current.headers].forEach(
column => {
// Give columns/headers rendering power
column.render = (type, userProps = {}) => {
const Comp = typeof type === 'string' ? column[type] : type
if (typeof Comp === 'undefined') {
throw new Error(renderErr)
}
return flexRender(Comp, {
...instanceRef.current,
...column,
...userProps,
})
}
return flexRender(Comp, {
...api,
...column,
...userProps,
})
// Give columns/headers a default getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_'),
colSpan: column.columns ? column.columns.length : 1,
},
applyPropHooks(
instanceRef.current.hooks.getHeaderProps,
column,
instanceRef.current
),
props
)
}
)
// Give columns/headers a default getHeaderProps
column.getHeaderProps = props =>
mergeProps(
{
key: ['header', column.id].join('_'),
colSpan: column.columns ? column.columns.length : 1,
},
applyPropHooks(api.hooks.getHeaderProps, column, api),
props
)
})
// // Allow hooks to decorate headerGroups
// if (debug) console.time('hooks.useHeaderGroups')
// instanceRef.current.headerGroups = applyHooks(
// instanceRef.current.hooks.useHeaderGroups,
// instanceRef.current.headerGroups,
// instanceRef.current
// )
// Allow hooks to decorate headerGroups
if (debug) console.time('hooks.headerGroups')
api.headerGroups = applyHooks(
api.hooks.headerGroups,
api.headerGroups,
api
).filter((headerGroup, i) => {
instanceRef.current.headerGroups.filter((headerGroup, i) => {
// Filter out any headers and headerGroups that don't have visible columns
headerGroup.headers = headerGroup.headers.filter(header => {
const recurse = columns =>
@@ -242,7 +276,11 @@ export const useTable = (props, ...plugins) => {
{
key: [`header${i}`].join('_'),
},
applyPropHooks(api.hooks.getHeaderGroupProps, headerGroup, api),
applyPropHooks(
instanceRef.current.hooks.getHeaderGroupProps,
headerGroup,
instanceRef.current
),
props
)
return true
@@ -250,29 +288,39 @@ export const useTable = (props, ...plugins) => {
return false
})
if (debug) console.timeEnd('hooks.headerGroups')
// if (debug) console.timeEnd('hooks.useHeaderGroups')
// Run the rows (this could be a dangerous hook with a ton of data)
if (debug) console.time('hooks.rows')
api.rows = applyHooks(api.hooks.rows, api.rows, api)
if (debug) console.timeEnd('hooks.rows')
if (debug) console.time('hooks.useRows')
instanceRef.current.rows = applyHooks(
instanceRef.current.hooks.useRows,
instanceRef.current.rows,
instanceRef.current
)
if (debug) console.timeEnd('hooks.useRows')
// The prepareRow function is absolutely necessary and MUST be called on
// any rows the user wishes to be displayed.
api.prepareRow = row => {
instanceRef.current.prepareRow = row => {
const { path } = row
row.getRowProps = props =>
mergeProps(
{ key: ['row', ...path].join('_') },
applyPropHooks(api.hooks.getRowProps, row, api),
applyPropHooks(
instanceRef.current.hooks.getRowProps,
row,
instanceRef.current
),
props
)
// need to apply any row specific hooks (useExpanded requires this)
applyHooks(api.hooks.row, row, api)
applyHooks(instanceRef.current.hooks.prepareRow, row, instanceRef.current)
const visibleColumns = api.columns.filter(column => column.visible)
const visibleColumns = instanceRef.current.columns.filter(
column => column.visible
)
// Build the cells for each row
row.cells = visibleColumns.map(column => {
@@ -289,7 +337,11 @@ export const useTable = (props, ...plugins) => {
{
key: ['cell', columnPathStr].join('_'),
},
applyPropHooks(api.hooks.getCellProps, cell, api),
applyPropHooks(
instanceRef.current.hooks.getCellProps,
cell,
instanceRef.current
),
props
)
}
@@ -303,7 +355,7 @@ export const useTable = (props, ...plugins) => {
}
return flexRender(Comp, {
...api,
...instanceRef.current,
...cell,
...userProps,
})
@@ -313,26 +365,14 @@ export const useTable = (props, ...plugins) => {
})
}
api.getTableProps = userProps =>
mergeProps(applyPropHooks(api.hooks.getTableProps, api), userProps)
instanceRef.current.getTableProps = userProps =>
mergeProps(
applyPropHooks(
instanceRef.current.hooks.getTableProps,
instanceRef.current
),
userProps
)
return api
}
function flattenBy(columns, childKey) {
const flatColumns = []
const recurse = columns => {
columns.forEach(d => {
if (!d[childKey]) {
flatColumns.push(d)
} else {
recurse(d[childKey])
}
})
}
recurse(columns)
return flatColumns
return instanceRef.current
}

View File

@@ -38,45 +38,48 @@ export const useExpanded = props => {
}, actions.toggleExpanded)
}
hooks.row.push(row => {
hooks.prepareRow.push(row => {
const { path } = row
row.toggleExpanded = set => toggleExpandedByPath(path, set)
return row
})
const expandedRows = useMemo(() => {
if (debug) console.info('getExpandedRows')
const expandedRows = useMemo(
() => {
if (debug) console.info('getExpandedRows')
const expandedRows = []
const expandedRows = []
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, row.index]
// Here we do some mutation, but it's the last stage in the
// immutable process so this is safe
const handleRow = (row, depth = 0, parentPath = []) => {
// Compute some final state for the row
const path = [...parentPath, row.index]
row.path = path
row.depth = depth
row.path = path
row.depth = depth
row.isExpanded =
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, path)
row.isExpanded =
(row.original && row.original[manualExpandedKey]) ||
getBy(expanded, path)
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
expandedRows.push(row)
if (paginateSubRows || (!paginateSubRows && row.depth === 0)) {
expandedRows.push(row)
}
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
}
return row
}
if (row.isExpanded && row.subRows && row.subRows.length) {
row.subRows.forEach((row, i) => handleRow(row, depth + 1, path))
}
rows.forEach(row => handleRow(row))
return row
}
rows.forEach(row => handleRow(row))
return expandedRows
}, [debug, rows, manualExpandedKey, expanded, paginateSubRows])
return expandedRows
},
[debug, rows, manualExpandedKey, expanded, paginateSubRows]
)
const expandedDepth = findExpandedDepth(expanded)

View File

@@ -22,8 +22,12 @@ const propTypes = {
manualFilters: PropTypes.bool,
}
export const useFilters = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useFilters')
export const useFilters = hooks => {
hooks.useMain.push(useMain)
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useFilters')
const {
debug,
@@ -32,9 +36,8 @@ export const useFilters = props => {
filterTypes: userFilterTypes,
manualFilters,
disableFilters,
hooks,
state: [{ filters }, setState],
} = props
} = instance
const setFilter = (id, updater) => {
const column = columns.find(d => d.id === id)
@@ -94,28 +97,24 @@ export const useFilters = props => {
}, actions.setAllFilters)
}
hooks.columns.push(columns => {
columns.forEach(column => {
const { id, accessor, disableFilters: columnDisableFilters } = column
columns.forEach(column => {
const { id, accessor, disableFilters: columnDisableFilters } = column
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
columnDisableFilters,
disableFilters === true ? false : undefined,
true
)
: false
// Determine if a column is filterable
column.canFilter = accessor
? getFirstDefined(
columnDisableFilters,
disableFilters === true ? false : undefined,
true
)
: false
// Provide the column a way of updating the filter value
column.setFilter = val => setFilter(column.id, val)
// Provide the column a way of updating the filter value
column.setFilter = val => setFilter(column.id, val)
// Provide the current filter value to the column for
// convenience
column.filterValue = filters[id]
})
return columns
// Provide the current filter value to the column for
// convenience
column.filterValue = filters[id]
})
// TODO: Create a filter cache for incremental high speed multi-filtering
@@ -199,7 +198,7 @@ export const useFilters = props => {
)
return {
...props,
...instance,
setFilter,
setAllFilters,
preFilteredRows: rows,

View File

@@ -48,7 +48,7 @@ export const useGroupBy = props => {
// Sort grouped columns to the start of the column list
// before the headers are built
hooks.columnsBeforeHeaderGroups.push(columns => {
hooks.useColumnsBeforeHeaderGroups.push(columns => {
// eslint-disable-next-line react-hooks/rules-of-hooks
return React.useMemo(
() => [
@@ -92,7 +92,7 @@ export const useGroupBy = props => {
}, actions.toggleGroupBy)
}
hooks.columns.push(columns => {
hooks.useColumns.push(columns => {
columns.forEach(column => {
if (column.canGroupBy) {
column.toggleGroupBy = () => toggleGroupBy(column.id)
@@ -128,8 +128,8 @@ export const useGroupBy = props => {
return columns
}
hooks.columns.push(addGroupByToggleProps)
hooks.headers.push(addGroupByToggleProps)
hooks.useColumns.push(addGroupByToggleProps)
hooks.useHeaders.push(addGroupByToggleProps)
const groupedRows = useMemo(
() => {

View File

@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import { addActions, actions } from '../actions'
@@ -34,8 +34,12 @@ const propTypes = {
disableMultiRemove: PropTypes.bool,
}
export const useSortBy = props => {
PropTypes.checkPropTypes(propTypes, props, 'property', 'useSortBy')
export const useSortBy = hooks => {
hooks.useMain.push(useMain)
}
function useMain(instance) {
PropTypes.checkPropTypes(propTypes, instance, 'property', 'useSortBy')
const {
debug,
@@ -51,7 +55,7 @@ export const useSortBy = props => {
hooks,
state: [{ sortBy }, setState],
plugins,
} = props
} = instance
// If useSortBy should probably come after useFilters for
// the best performance, so let's hint to the user about that...
@@ -67,16 +71,8 @@ export const useSortBy = props => {
)
}
columns.forEach(column => {
const { accessor, disableSorting: columnDisableSorting } = column
column.canSortBy = accessor
? getFirstDefined(
columnDisableSorting,
disableSorting === true ? false : undefined,
true
)
: false
})
// Add custom hooks
hooks.getSortByToggleProps = []
// Updates sorting based on a columnID, desc flag and multi flag
const toggleSortBy = (columnID, desc, multi) => {
@@ -164,63 +160,58 @@ export const useSortBy = props => {
}, actions.sortByChange)
}
hooks.columns.push(columns => {
columns.forEach(column => {
if (column.canSortBy) {
column.toggleSortBy = (desc, multi) =>
toggleSortBy(column.id, desc, multi)
}
})
return columns
})
// Add the getSortByToggleProps method to columns and headers
;[...instance.columns, ...instance.headers].forEach(column => {
const { accessor, disableSorting: columnDisableSorting, id } = column
hooks.getSortByToggleProps = []
const addSortByToggleProps = (columns, api) => {
columns.forEach(column => {
const { canSortBy } = column
column.getSortByToggleProps = props => {
return mergeProps(
{
onClick: canSortBy
? e => {
e.persist()
column.toggleSortBy(
undefined,
!api.disableMultiSort && e.shiftKey
)
}
: undefined,
style: {
cursor: canSortBy ? 'pointer' : undefined,
},
title: 'Toggle SortBy',
},
applyPropHooks(api.hooks.getSortByToggleProps, column, api),
props
const canSort = accessor
? getFirstDefined(
columnDisableSorting,
disableSorting === true ? false : undefined,
true
)
}
})
return columns
}
: false
hooks.columns.push(addSortByToggleProps)
hooks.headers.push(addSortByToggleProps)
column.canSort = canSort
if (column.canSort) {
column.toggleSortBy = (desc, multi) =>
toggleSortBy(column.id, desc, multi)
}
column.getSortByToggleProps = props => {
return mergeProps(
{
onClick: canSort
? e => {
e.persist()
column.toggleSortBy(
undefined,
!instance.disableMultiSort && e.shiftKey
)
}
: undefined,
style: {
cursor: canSort ? 'pointer' : undefined,
},
title: 'Toggle SortBy',
},
applyPropHooks(instance.hooks.getSortByToggleProps, column, instance),
props
)
}
// Mutate columns to reflect sorting state
columns.forEach(column => {
const { id } = column
column.sorted = sortBy.find(d => d.id === id)
column.sortedIndex = sortBy.findIndex(d => d.id === id)
column.sortedDesc = column.sorted ? column.sorted.desc : undefined
})
const sortedRows = useMemo(
const sortedRows = React.useMemo(
() => {
if (manualSorting || !sortBy.length) {
return rows
}
if (debug) console.info('getSortedRows')
if (debug) console.time('getSortedRows')
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
@@ -270,6 +261,8 @@ export const useSortBy = props => {
row.subRows = sortData(row.subRows)
})
if (debug) console.timeEnd('getSortedRows')
return sortedData
}
@@ -279,7 +272,7 @@ export const useSortBy = props => {
)
return {
...props,
...instance,
toggleSortBy,
rows: sortedRows,
preSortedRows: rows,

View File

@@ -260,7 +260,15 @@ export const mergeProps = (...groups) => {
}
export const applyHooks = (hooks, initial, ...args) =>
hooks.reduce((prev, next) => next(prev, ...args), initial)
hooks.reduce((prev, next) => {
const nextValue = next(prev, ...args)
if (typeof nextValue === 'undefined') {
throw new Error(
'React Table: A hook just returned undefined! This is not allowed.'
)
}
return nextValue
}, initial)
export const applyPropHooks = (hooks, ...args) =>
hooks.reduce((prev, next) => mergeProps(prev, next(...args)), {})
@@ -285,6 +293,24 @@ export function isFunction(a) {
}
}
export function flattenBy(columns, childKey) {
const flatColumns = []
const recurse = columns => {
columns.forEach(d => {
if (!d[childKey]) {
flatColumns.push(d)
} else {
recurse(d[childKey])
}
})
}
recurse(columns)
return flatColumns
}
//
function makePathArray(obj) {