diff --git a/examples/filtering/sandbox.config.json b/examples/filtering/sandbox.config.json new file mode 100644 index 0000000..0ef8d24 --- /dev/null +++ b/examples/filtering/sandbox.config.json @@ -0,0 +1,3 @@ +{ + "infiniteLoopProtection": false +} diff --git a/examples/filtering/src/App.js b/examples/filtering/src/App.js index 80d31f6..635f33a 100644 --- a/examples/filtering/src/App.js +++ b/examples/filtering/src/App.js @@ -36,7 +36,9 @@ const Styles = styled.div` ` // Define a default UI for filtering -function DefaultColumnFilter({ filterValue, preFilteredRows, setFilter }) { +function DefaultColumnFilter({ + column: { filterValue, preFilteredRows, setFilter }, +}) { const count = preFilteredRows.length return ( @@ -52,19 +54,18 @@ function DefaultColumnFilter({ filterValue, preFilteredRows, setFilter }) { // This is a custom filter UI for selecting // a unique option from a list -function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { +function SelectColumnFilter({ + column: { filterValue, setFilter, preFilteredRows, id }, +}) { // Calculate the options for filtering // using the preFilteredRows - const options = React.useMemo( - () => { - const options = new Set() - preFilteredRows.forEach(row => { - options.add(row.values[id]) - }) - return [...options.values()] - }, - [id, preFilteredRows] - ) + const options = React.useMemo(() => { + const options = new Set() + preFilteredRows.forEach(row => { + options.add(row.values[id]) + }) + return [...options.values()] + }, [id, preFilteredRows]) // Render a multi-select box return ( @@ -87,22 +88,21 @@ function SelectColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { // This is a custom filter UI that uses a // slider to set the filter value between a column's // min and max values -function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { +function SliderColumnFilter({ + column: { filterValue, setFilter, preFilteredRows, id }, +}) { // Calculate the min and max // using the preFilteredRows - const [min, max] = React.useMemo( - () => { - let min = 0 - let max = 0 - preFilteredRows.forEach(row => { - min = Math.min(row.values[id], min) - max = Math.max(row.values[id], max) - }) - return [min, max] - }, - [id, preFilteredRows] - ) + const [min, max] = React.useMemo(() => { + let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + preFilteredRows.forEach(row => { + min = Math.min(row.values[id], min) + max = Math.max(row.values[id], max) + }) + return [min, max] + }, [id, preFilteredRows]) return ( <> @@ -124,23 +124,17 @@ function SliderColumnFilter({ filterValue, setFilter, preFilteredRows, id }) { // filter. It uses two number boxes and filters rows to // ones that have values between the two function NumberRangeColumnFilter({ - filterValue = [], - preFilteredRows, - setFilter, - id, + column: { filterValue = [], preFilteredRows, setFilter, id }, }) { - const [min, max] = React.useMemo( - () => { - let min = 0 - let max = 0 - preFilteredRows.forEach(row => { - min = Math.min(row.values[id], min) - max = Math.max(row.values[id], max) - }) - return [min, max] - }, - [id, preFilteredRows] - ) + const [min, max] = React.useMemo(() => { + let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 + preFilteredRows.forEach(row => { + min = Math.min(row.values[id], min) + max = Math.max(row.values[id], max) + }) + return [min, max] + }, [id, preFilteredRows]) return (
@@ -337,7 +331,7 @@ function App() { [] ) - const data = React.useMemo(() => makeData(10000), []) + const data = React.useMemo(() => makeData(100000), []) return ( diff --git a/package.json b/package.json index 84bb82e..fffa804 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "jest-watch-select-projects": "^0.1.2", "jest-watch-typeahead": "^0.3.1", "lint-staged": "^9.2.1", + "prettier": "^1.18.2", "prop-types": "^15.5.0", "react": "^16.9.0", "react-dom": "^16.9.0", diff --git a/src/hooks/useTable.js b/src/hooks/useTable.js index dd4d684..5c486b4 100755 --- a/src/hooks/useTable.js +++ b/src/hooks/useTable.js @@ -102,30 +102,27 @@ export const useTable = (props, ...plugins) => { ]) // Allow hooks to decorate columns (and trigger this memoization via deps) - columns = React.useMemo( - () => { - if (process.env.NODE_ENV === 'development' && debug) - console.time('hooks.columnsBeforeHeaderGroups') - const newColumns = applyHooks( - instanceRef.current.hooks.columnsBeforeHeaderGroups, - columns, - instanceRef.current - ) - if (process.env.NODE_ENV === 'development' && debug) - console.timeEnd('hooks.columnsBeforeHeaderGroups') - return newColumns - }, - [ + columns = React.useMemo(() => { + if (process.env.NODE_ENV === 'development' && debug) + console.time('hooks.columnsBeforeHeaderGroups') + const newColumns = applyHooks( + instanceRef.current.hooks.columnsBeforeHeaderGroups, columns, - debug, - // eslint-disable-next-line react-hooks/exhaustive-deps - ...applyHooks( - instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps, - [], - instanceRef.current - ), - ] - ) + instanceRef.current + ) + if (process.env.NODE_ENV === 'development' && debug) + console.timeEnd('hooks.columnsBeforeHeaderGroups') + return newColumns + }, [ + columns, + debug, + // eslint-disable-next-line react-hooks/exhaustive-deps + ...applyHooks( + instanceRef.current.hooks.columnsBeforeHeaderGroupsDeps, + [], + instanceRef.current + ), + ]) // Make the headerGroups const headerGroups = React.useMemo( @@ -144,72 +141,69 @@ export const useTable = (props, ...plugins) => { }) // Access the row model - const [rows, rowPaths, flatRows] = React.useMemo( - () => { - if (process.env.NODE_ENV === 'development' && debug) - console.time('getAccessedRows') + const [rows, rowPaths, flatRows] = React.useMemo(() => { + if (process.env.NODE_ENV === 'development' && debug) + console.time('getAccessedRows') - let flatRows = 0 - const rowPaths = [] + let flatRows = 0 + const rowPaths = [] - // Access the row's data - const accessRow = (originalRow, i, depth = 0, parentPath = []) => { - // Keep the original reference around - const original = originalRow + // Access the row's data + const accessRow = (originalRow, i, depth = 0, parentPath = []) => { + // Keep the original reference around + const original = originalRow - // Make the new path for the row - const path = [...parentPath, i] + // Make the new path for the row + const path = [...parentPath, i] - flatRows++ - rowPaths.push(path.join('.')) + flatRows++ + rowPaths.push(path.join('.')) - // Process any subRows - const subRows = originalRow[subRowsKey] - ? originalRow[subRowsKey].map((d, i) => - accessRow(d, i, depth + 1, path) - ) - : [] - - const row = { - original, - index: i, - path, // used to create a key for each row even if not nested - subRows, - depth, - cells: [{}], // This is a dummy cell - } - - // Override common array functions (and the dummy cell's getCellProps function) - // to show an error if it is accessed without calling prepareRow - const unpreparedAccessWarning = () => { - throw new Error( - 'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.' + // Process any subRows + const subRows = originalRow[subRowsKey] + ? originalRow[subRowsKey].map((d, i) => + accessRow(d, i, depth + 1, path) ) - } - row.cells.map = unpreparedAccessWarning - row.cells.filter = unpreparedAccessWarning - row.cells.forEach = unpreparedAccessWarning - row.cells[0].getCellProps = unpreparedAccessWarning + : [] - // Create the cells and values - row.values = {} - instanceRef.current.columns.forEach(column => { - row.values[column.id] = column.accessor - ? column.accessor(originalRow, i, { subRows, depth, data }) - : undefined - }) - - return row + const row = { + original, + index: i, + path, // used to create a key for each row even if not nested + subRows, + depth, + cells: [{}], // This is a dummy cell } - // Use the resolved data - const accessedData = data.map((d, i) => accessRow(d, i)) - if (process.env.NODE_ENV === 'development' && debug) - console.timeEnd('getAccessedRows') - return [accessedData, rowPaths, flatRows] - }, - [debug, data, subRowsKey] - ) + // Override common array functions (and the dummy cell's getCellProps function) + // to show an error if it is accessed without calling prepareRow + const unpreparedAccessWarning = () => { + throw new Error( + 'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.' + ) + } + row.cells.map = unpreparedAccessWarning + row.cells.filter = unpreparedAccessWarning + row.cells.forEach = unpreparedAccessWarning + row.cells[0].getCellProps = unpreparedAccessWarning + + // Create the cells and values + row.values = {} + instanceRef.current.columns.forEach(column => { + row.values[column.id] = column.accessor + ? column.accessor(originalRow, i, { subRows, depth, data }) + : undefined + }) + + return row + } + + // Use the resolved data + const accessedData = data.map((d, i) => accessRow(d, i)) + if (process.env.NODE_ENV === 'development' && debug) + console.timeEnd('getAccessedRows') + return [accessedData, rowPaths, flatRows] + }, [debug, data, subRowsKey]) instanceRef.current.rows = rows instanceRef.current.rowPaths = rowPaths diff --git a/src/plugin-hooks/useFilters.js b/src/plugin-hooks/useFilters.js index 4325161..075f6a9 100755 --- a/src/plugin-hooks/useFilters.js +++ b/src/plugin-hooks/useFilters.js @@ -129,92 +129,87 @@ function useMain(instance) { // cache for each row group (top-level rows, and each row's recursive subrows) // This would make multi-filtering a lot faster though. Too far? - const filteredRows = React.useMemo( - () => { - if (manualFilters || !Object.keys(filters).length) { - return rows - } + const filteredRows = React.useMemo(() => { + if (manualFilters || !Object.keys(filters).length) { + return rows + } - if (process.env.NODE_ENV === 'development' && debug) - console.info('getFilteredRows') + if (process.env.NODE_ENV === 'development' && debug) + console.info('getFilteredRows') - // Filters top level and nested rows - const filterRows = (rows, depth = 0) => { - let filteredRows = rows + // Filters top level and nested rows + const filterRows = (rows, depth = 0) => { + let filteredRows = rows - filteredRows = Object.entries(filters).reduce( - (filteredSoFar, [columnID, filterValue]) => { - // Find the filters column - const column = columns.find(d => d.id === columnID) + filteredRows = Object.entries(filters).reduce( + (filteredSoFar, [columnID, filterValue]) => { + // Find the filters column + const column = columns.find(d => d.id === columnID) - if (depth === 0) { - column.preFilteredRows = filteredSoFar - } + if (depth === 0) { + column.preFilteredRows = filteredSoFar + } - if (!column) { - return filteredSoFar - } + if (!column) { + return filteredSoFar + } - const filterMethod = getFilterMethod( - column.filter, - userFilterTypes || {}, - filterTypes + const filterMethod = getFilterMethod( + column.filter, + userFilterTypes || {}, + filterTypes + ) + + if (!filterMethod) { + console.warn( + `Could not find a valid 'column.filter' for column with the ID: ${column.id}.` ) - - if (!filterMethod) { - console.warn( - `Could not find a valid 'column.filter' for column with the ID: ${ - column.id - }.` - ) - return filteredSoFar - } - - // Pass the rows, id, filterValue and column to the filterMethod - // to get the filtered rows back - return filterMethod(filteredSoFar, columnID, filterValue, column) - }, - rows - ) - - // Apply the filter to any subRows - // We technically could do this recursively in the above loop, - // but that would severely hinder the API for the user, since they - // would be required to do that recursion in some scenarios - filteredRows = filteredRows.map(row => { - if (!row.subRows) { - return row + return filteredSoFar } - return { - ...row, - subRows: - row.subRows && row.subRows.length > 0 - ? filterRows(row.subRows, depth + 1) - : row.subRows, - } - }) - return filteredRows - } - - const filteredRows = filterRows(rows) - - // Now that each filtered column has it's partially filtered rows, - // lets assign the final filtered rows to all of the other columns - const nonFilteredColumns = columns.filter( - column => !Object.keys(filters).includes(column.id) + // Pass the rows, id, filterValue and column to the filterMethod + // to get the filtered rows back + return filterMethod(filteredSoFar, columnID, filterValue, column) + }, + rows ) - // This essentially enables faceted filter options to be built easily - // using every column's preFilteredRows value - nonFilteredColumns.forEach(column => { - column.preFilteredRows = filteredRows + // Apply the filter to any subRows + // We technically could do this recursively in the above loop, + // but that would severely hinder the API for the user, since they + // would be required to do that recursion in some scenarios + filteredRows = filteredRows.map(row => { + if (!row.subRows) { + return row + } + return { + ...row, + subRows: + row.subRows && row.subRows.length > 0 + ? filterRows(row.subRows, depth + 1) + : row.subRows, + } }) return filteredRows - }, - [manualFilters, filters, debug, rows, columns, userFilterTypes] - ) + } + + return filterRows(rows) + }, [manualFilters, filters, debug, rows, columns, userFilterTypes]) + + React.useMemo(() => { + // Now that each filtered column has it's partially filtered rows, + // lets assign the final filtered rows to all of the other columns + const nonFilteredColumns = columns.filter( + column => !Object.keys(filters).includes(column.id) + ) + + // This essentially enables faceted filter options to be built easily + // using every column's preFilteredRows value + nonFilteredColumns.forEach(column => { + column.preFilteredRows = filteredRows + }) + }, [columns, filteredRows, filters]) return { ...instance, diff --git a/src/plugin-hooks/usePagination.js b/src/plugin-hooks/usePagination.js index 341e2de..b3810ca 100755 --- a/src/plugin-hooks/usePagination.js +++ b/src/plugin-hooks/usePagination.js @@ -52,46 +52,40 @@ function useMain(instance) { const isPageIndexMountedRef = React.useRef() - useLayoutEffect( - () => { - if (isPageIndexMountedRef.current) { - setState( - old => ({ - ...old, - pageIndex: 0, - }), - actions.pageChange - ) - } - isPageIndexMountedRef.current = true - }, - [setState, rowDep, filters, groupBy, sortBy] - ) + useLayoutEffect(() => { + if (isPageIndexMountedRef.current) { + setState( + old => ({ + ...old, + pageIndex: 0, + }), + actions.pageChange + ) + } + isPageIndexMountedRef.current = true + }, [setState, rowDep, filters, groupBy, sortBy]) - const pages = React.useMemo( - () => { - if (manualPagination) { - return undefined - } - if (process.env.NODE_ENV === 'development' && debug) - console.info('getPages') + const pages = React.useMemo(() => { + if (manualPagination) { + return undefined + } + if (process.env.NODE_ENV === 'development' && debug) + console.info('getPages') - // Create a new pages with the first page ready to go. - const pages = rows.length ? [] : [[]] + // Create a new pages with the first page ready to go. + const pages = rows.length ? [] : [[]] - // Start the pageIndex and currentPage cursors - let cursor = 0 + // Start the pageIndex and currentPage cursors + let cursor = 0 - while (cursor < rows.length) { - const end = cursor + pageSize - pages.push(rows.slice(cursor, end)) - cursor = end - } + while (cursor < rows.length) { + const end = cursor + pageSize + pages.push(rows.slice(cursor, end)) + cursor = end + } - return pages - }, - [debug, manualPagination, pageSize, rows] - ) + return pages + }, [debug, manualPagination, pageSize, rows]) const pageCount = manualPagination ? userPageCount : pages.length diff --git a/src/plugin-hooks/useRowState.js b/src/plugin-hooks/useRowState.js index eefd247..5d2c92d 100644 --- a/src/plugin-hooks/useRowState.js +++ b/src/plugin-hooks/useRowState.js @@ -72,21 +72,18 @@ function useMain(instance) { const rowsMountedRef = React.useRef() // When data changes, reset row and cell state - React.useEffect( - () => { - if (rowsMountedRef.current) { - setState(old => { - return { - ...old, - rowState: {}, - } - }, actions.setRowState) - } + React.useEffect(() => { + if (rowsMountedRef.current) { + setState(old => { + return { + ...old, + rowState: {}, + } + }, actions.setRowState) + } - rowsMountedRef.current = true - }, - [rows, setState] - ) + rowsMountedRef.current = true + }, [rows, setState]) hooks.prepareRow.push(row => { const pathKey = row.path.join('.') diff --git a/src/plugin-hooks/useSortBy.js b/src/plugin-hooks/useSortBy.js index 3008ccf..e602a80 100755 --- a/src/plugin-hooks/useSortBy.js +++ b/src/plugin-hooks/useSortBy.js @@ -202,72 +202,69 @@ function useMain(instance) { column.sortedDesc = column.sorted ? column.sorted.desc : undefined }) - const sortedRows = React.useMemo( - () => { - if (manualSorting || !sortBy.length) { - return rows - } - if (process.env.NODE_ENV === 'development' && debug) - console.time('getSortedRows') + const sortedRows = React.useMemo(() => { + if (manualSorting || !sortBy.length) { + return rows + } + if (process.env.NODE_ENV === 'development' && debug) + console.time('getSortedRows') - const sortData = rows => { - // Use the orderByFn to compose multiple sortBy's together. - // This will also perform a stable sorting using the row index - // if needed. - const sortedData = orderByFn( - rows, - sortBy.map(sort => { - // Support custom sorting methods for each column - const { sortType } = columns.find(d => d.id === sort.id) + const sortData = rows => { + // Use the orderByFn to compose multiple sortBy's together. + // This will also perform a stable sorting using the row index + // if needed. + const sortedData = orderByFn( + rows, + sortBy.map(sort => { + // Support custom sorting methods for each column + const { sortType } = columns.find(d => d.id === sort.id) - // Look up sortBy functions in this order: - // column function - // column string lookup on user sortType - // column string lookup on built-in sortType - // default function - // default string lookup on user sortType - // default string lookup on built-in sortType - const sortMethod = - isFunction(sortType) || - (userSortTypes || {})[sortType] || - sortTypes[sortType] || - sortTypes.alphanumeric + // Look up sortBy functions in this order: + // column function + // column string lookup on user sortType + // column string lookup on built-in sortType + // default function + // default string lookup on user sortType + // default string lookup on built-in sortType + const sortMethod = + isFunction(sortType) || + (userSortTypes || {})[sortType] || + sortTypes[sortType] || + sortTypes.alphanumeric - // Return the correct sortFn - return (a, b) => - sortMethod(a.values[sort.id], b.values[sort.id], sort.desc) - }), - // Map the directions - sortBy.map(sort => { - // Detect and use the sortInverted option - const { sortInverted } = columns.find(d => d.id === sort.id) + // Return the correct sortFn + return (a, b) => + sortMethod(a.values[sort.id], b.values[sort.id], sort.desc) + }), + // Map the directions + sortBy.map(sort => { + // Detect and use the sortInverted option + const { sortInverted } = columns.find(d => d.id === sort.id) - if (sortInverted) { - return sort.desc - } - - return !sort.desc - }) - ) - - // If there are sub-rows, sort them - sortedData.forEach(row => { - if (!row.subRows || row.subRows.length <= 1) { - return + if (sortInverted) { + return sort.desc } - row.subRows = sortData(row.subRows) + + return !sort.desc }) + ) - return sortedData - } + // If there are sub-rows, sort them + sortedData.forEach(row => { + if (!row.subRows || row.subRows.length <= 1) { + return + } + row.subRows = sortData(row.subRows) + }) - if (process.env.NODE_ENV === 'development' && debug) - console.timeEnd('getSortedRows') + return sortedData + } - return sortData(rows) - }, - [manualSorting, sortBy, debug, columns, rows, orderByFn, userSortTypes] - ) + if (process.env.NODE_ENV === 'development' && debug) + console.timeEnd('getSortedRows') + + return sortData(rows) + }, [manualSorting, sortBy, debug, columns, rows, orderByFn, userSortTypes]) return { ...instance, diff --git a/yarn.lock b/yarn.lock index 6a4e5a8..82da0a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6320,6 +6320,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + pretty-format@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760"