diff --git a/.size-snapshot.json b/.size-snapshot.json index 427b197..051eb6b 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,20 +1,20 @@ { "dist/index.js": { - "bundled": 103833, - "minified": 48853, - "gzipped": 12789 + "bundled": 106450, + "minified": 50539, + "gzipped": 13258 }, "dist/index.es.js": { - "bundled": 102990, - "minified": 48103, - "gzipped": 12638, + "bundled": 105607, + "minified": 49789, + "gzipped": 13104, "treeshaked": { "rollup": { "code": 80, "import_statements": 21 }, "webpack": { - "code": 8444 + "code": 8904 } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 596805c..f8256fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 7.0.0-rc.7 + +- The exported (but undocumented) `applyHooks` function has been deprecated. Please use either `reduceHooks` or `loopHooks` utilities in your custom plugins now. +- The exported (but undocumented) `applyPropHooks` function has been deprecated. Please use the `makePropGetter` utility in your custom plugins now. +- Added the `reduceHooks` exported utility which is used to reduce a value through a collection of hooks. Each hook must return a value (mutation is discouraged) +- Added the `loopHooks` exported utility which is used to loop over a collection of hooks. Hooks are not allowed to return a value (mutation is encouraged) +- Prop-getter hook functions now support returning an array (in addition to the typical object of props). When an array is returned, each item in the array is smart-merged into a new props object (meaning it will intelligently compose and override styles and className) +- Added the `makePropGetter` exported utility which is used to create prop getters from a prop getter hook. +- Prop-getter function supplied to the table have 2 new overloaded options (in addition to the typical object of props): + - `Function(props, instance, ...row/col/context) => Array | props` - If a function is passed to a prop getter function, it will receive the previous props, the table instance, and potentially more context arguments. It is then be expected to return either an array of new props (to be smart-merged with styles and classes, the latest values taking priority over the previous values) or a props object (which will replace all previous props) + - `Array` - If an array is passed to a prop getter function, each prop object in the array will be smart-merged with styles and classes into the props from previous hooks (with the latest values taking priority over the previous values). +- Extracted default hooks into separate file. +- Added the `useOptions` plugin hook, which allows a plugin to reduce/modify the initial options being passed to the table +- Converted almost all usages of `instanceRef.current` to use `useGetLatest(instanceRef.current)` to help with avoiding memory leaks and to be more terse. +- Converted all previous prop-getter definitions to use the new `makePropGetter` +- Reorganized plugin hooks to declare as many hooks in the main plugin function as opposed to in the `useInstance` hook. +- Changed the `useInstanceBeforeDimensions` hook to be a `loopHooks` call instead of a reducer. An error will be thrown now if any of these hook functions returns a value (to discourage mutation of the instance) +- Changed the `useInstance` hook to be a `loopHooks` call instead of a reducer. An error will be thrown now if any of these hook functions returns a value (to discourage mutation of the instance) +- Change the `prepareRow` hook to be a `loopHooks` call instead of a reducer. An error will be thrown now if any of these hook functions returns a value (to discourage mutation of the row) + ## 7.0.0-rc.6 - The `columnsBeforeHeaderGroups` and `columnsBeforeHeaderGroupsDeps` hooks have been renamed to `flatColumns` and `flatColumnsDeps` respectively, which better reflects what they are used for, rather than their order, which can remain implicit. diff --git a/examples/data-driven-classes-and-styles/src/App.js b/examples/data-driven-classes-and-styles/src/App.js index 2325c12..f8f77e2 100644 --- a/examples/data-driven-classes-and-styles/src/App.js +++ b/examples/data-driven-classes-and-styles/src/App.js @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import { useTable, mergeProps } from 'react-table' +import { useTable } from 'react-table' import makeData from './makeData' @@ -68,16 +68,15 @@ function Table({ {headerGroup.headers.map(column => ( {column.render('Header')} @@ -89,21 +88,20 @@ function Table({ {rows.map((row, i) => { prepareRow(row) return ( - // Merge row props - + // Merge user row props in + {row.cells.map(cell => { return ( {cell.render('Cell')} diff --git a/examples/kitchen-sink/yarn.lock b/examples/kitchen-sink/yarn.lock index 674c38c..e16b323 100644 --- a/examples/kitchen-sink/yarn.lock +++ b/examples/kitchen-sink/yarn.lock @@ -8218,10 +8218,10 @@ react-scripts@3.0.1: optionalDependencies: fsevents "2.0.6" -react-table@next: - version "7.0.0-beta.19" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-beta.19.tgz#8563ae56f693cbffa10060772ba1f1cd4e926bb2" - integrity sha512-BRt7zW7PGyg67fR2CK9M2fwP6BocjQN870w3P+K+vDJMkpewGjSPDscCU5sJMBqCEjKWg85k2pVkiwQPg9ofgQ== +react-table@latest: + version "7.0.0-rc.6" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.6.tgz#3cb43919e31166659d6aa223bd3dfee765981f6b" + integrity sha512-iFgxMla6JnxjZwKG3eiHHOlIIANiK/HzvRFWXT8LV78N9bEOGC5A5N44S/xQ1PaZN/1c+NDBz5fBazhx/vG7/A== react@^16.8.6: version "16.12.0" diff --git a/examples/pagination-controlled/src/App.js b/examples/pagination-controlled/src/App.js index 9e38f47..d4ed994 100644 --- a/examples/pagination-controlled/src/App.js +++ b/examples/pagination-controlled/src/App.js @@ -133,9 +133,9 @@ function Table({ {loading ? ( // Use our custom loading state to show a loading indicator - Loading... + Loading... ) : ( - + Showing {page.length} of ~{controlledPageCount * pageSize}{' '} results diff --git a/examples/row-selection/yarn.lock b/examples/row-selection/yarn.lock index fa7e1dc..528c8aa 100644 --- a/examples/row-selection/yarn.lock +++ b/examples/row-selection/yarn.lock @@ -8087,10 +8087,10 @@ react-scripts@3.0.1: optionalDependencies: fsevents "2.0.6" -react-table@next: - version "7.0.0-alpha.7" - resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-alpha.7.tgz#0cb6da6f32adb397e68505b7cdd4880d15d73017" - integrity sha512-oXE9RRkE2CFk1OloNCSTPQ9qxOdujgkCoW5b/srbJsBog/ySkWuozBTQkxH1wGNmnSxGyTrTxJqXdXPQam7VAw== +react-table@latest: + version "7.0.0-rc.6" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.6.tgz#3cb43919e31166659d6aa223bd3dfee765981f6b" + integrity sha512-iFgxMla6JnxjZwKG3eiHHOlIIANiK/HzvRFWXT8LV78N9bEOGC5A5N44S/xQ1PaZN/1c+NDBz5fBazhx/vG7/A== react@^16.8.6: version "16.8.6" diff --git a/src/hooks/useColumnVisibility.js b/src/hooks/useColumnVisibility.js index e9a9651..3327a3d 100644 --- a/src/hooks/useColumnVisibility.js +++ b/src/hooks/useColumnVisibility.js @@ -3,9 +3,9 @@ import React from 'react' import { actions, functionalUpdate, - mergeProps, - applyPropHooks, useGetLatest, + useConsumeHookGetter, + makePropGetter, } from '../utils' actions.resetHiddenColumns = 'resetHiddenColumns' @@ -14,8 +14,8 @@ actions.setHiddenColumns = 'setHiddenColumns' actions.toggleHideAllColumns = 'toggleHideAllColumns' export const useColumnVisibility = hooks => { - hooks.getToggleHiddenProps = [] - hooks.getToggleHideAllColumnsProps = [] + hooks.getToggleHiddenProps = [defualtGetToggleHiddenProps] + hooks.getToggleHideAllColumnsProps = [defualtGetToggleHideAllColumnsProps] hooks.stateReducers.push(reducer) hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions) @@ -28,6 +28,36 @@ export const useColumnVisibility = hooks => { useColumnVisibility.pluginName = 'useColumnVisibility' +const defualtGetToggleHiddenProps = (props, instance, column) => [ + props, + { + onChange: e => { + column.toggleHidden(!e.target.checked) + }, + style: { + cursor: 'pointer', + }, + checked: column.isVisible, + title: 'Toggle Column Visible', + }, +] + +const defualtGetToggleHideAllColumnsProps = (props, instance) => [ + props, + { + onChange: e => { + instance.toggleHideAllColumns(!e.target.checked) + }, + style: { + cursor: 'pointer', + }, + checked: !instance.allColumnsHidden && !instance.state.hiddenColumns.length, + title: 'Toggle All Columns Hidden', + indeterminate: + !instance.allColumnsHidden && instance.state.hiddenColumns.length, + }, +] + function reducer(state, action, previousState, instanceRef) { if (action.type === actions.init) { return { @@ -111,8 +141,6 @@ function useInstanceBeforeDimensions(instance) { headers.forEach( subHeader => (totalVisibleHeaderCount += handleColumn(subHeader, true)) ) - - return instance } function useInstance(instance) { @@ -127,33 +155,6 @@ function useInstance(instance) { const allColumnsHidden = flatColumns.length === hiddenColumns.length - flatHeaders.forEach(column => { - column.toggleHidden = value => { - dispatch({ - type: actions.toggleHideColumn, - columnId: column.id, - value, - }) - } - - column.getToggleHiddenProps = props => { - return mergeProps( - { - onChange: e => { - column.toggleHidden(!e.target.checked) - }, - style: { - cursor: 'pointer', - }, - checked: column.isVisible, - title: 'Toggle Column Visible', - }, - applyPropHooks(getInstance().hooks.getToggleHiddenProps, getInstance()), - props - ) - } - }) - const toggleHideColumn = React.useCallback( (columnId, value) => dispatch({ type: actions.toggleHideColumn, columnId, value }), @@ -170,32 +171,44 @@ function useInstance(instance) { [dispatch] ) - const getToggleHideAllColumnsProps = props => { - return mergeProps( - { - onChange: e => { - toggleHideAllColumns(!e.target.checked) - }, - style: { - cursor: 'pointer', - }, - checked: !allColumnsHidden && !hiddenColumns.length, - title: 'Toggle All Columns Hidden', - indeterminate: !allColumnsHidden && hiddenColumns.length, - }, - applyPropHooks( - getInstance().hooks.getToggleHideAllColumnsProps, - getInstance() - ), - props - ) - } + // Snapshot hook and disallow more from being added + const getToggleHideAllColumnsPropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getToggleHideAllColumnsProps' + ) - return { - ...instance, + const getToggleHideAllColumnsProps = makePropGetter( + getToggleHideAllColumnsPropsHooks(), + getInstance() + ) + + // Snapshot hook and disallow more from being added + const getToggleHiddenPropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getToggleHiddenProps' + ) + + flatHeaders.forEach(column => { + column.toggleHidden = value => { + dispatch({ + type: actions.toggleHideColumn, + columnId: column.id, + value, + }) + } + + column.getToggleHiddenProps = makePropGetter( + getToggleHiddenPropsHooks(), + getInstance(), + column + ) + }) + + Object.assign(instance, { + allColumnsHidden, toggleHideColumn, setHiddenColumns, toggleHideAllColumns, getToggleHideAllColumnsProps, - } + }) } diff --git a/src/hooks/useTable.js b/src/hooks/useTable.js index 0d55a39..f4f739f 100755 --- a/src/hooks/useTable.js +++ b/src/hooks/useTable.js @@ -3,9 +3,9 @@ import React from 'react' // import { actions, - applyHooks, - applyPropHooks, - mergeProps, + reduceHooks, + loopHooks, + makePropGetter, flexRender, decorateColumnTree, makeHeaderGroups, @@ -14,6 +14,8 @@ import { useConsumeHookGetter, } from '../utils' +import makeDefaultPluginHooks from '../makeDefaultPluginHooks' + import { useColumnVisibility } from './useColumnVisibility' let renderErr = 'Renderer Error' @@ -25,64 +27,83 @@ const defaultGetSubRows = (row, index) => row.subRows || [] const defaultGetRowId = (row, index) => index const defaultUseControlledState = d => d -export const useTable = (props, ...plugins) => { - // Destructure props - let { - data, - columns: userColumns, +function applyDefaults(props) { + const { initialState = defaultInitialState, defaultColumn = defaultColumnInstance, getSubRows = defaultGetSubRows, getRowId = defaultGetRowId, - stateReducer: userStateReducer = defaultReducer, + stateReducer = defaultReducer, useControlledState = defaultUseControlledState, + ...rest } = props + return { + ...rest, + initialState, + defaultColumn, + getSubRows, + getRowId, + stateReducer, + useControlledState, + } +} + +export const useTable = (props, ...plugins) => { + // Apply default props + props = applyDefaults(props) + + // Add core plugins plugins = [useColumnVisibility, ...plugins] - // The table instance + // Create the table instance let instanceRef = React.useRef({}) - Object.assign(instanceRef.current, { + // Create a getter for the instance (helps avoid a lot of potential memory leaks) + const getInstance = useGetLatest(instanceRef.current) + + // Assign the props, plugins and hooks to the instance + Object.assign(getInstance(), { ...props, plugins, - data, - hooks: { - stateReducers: [], - columns: [], - columnsDeps: [], - flatColumns: [], - flatColumnsDeps: [], - headerGroups: [], - headerGroupsDeps: [], - useInstanceBeforeDimensions: [], - useInstance: [], - useRows: [], - prepareRow: [], - getTableProps: [], - getTableBodyProps: [], - getRowProps: [], - getHeaderGroupProps: [], - getFooterGroupProps: [], - getHeaderProps: [], - getFooterProps: [], - getCellProps: [], - }, + hooks: makeDefaultPluginHooks(), }) // Allow plugins to register hooks as early as possible plugins.filter(Boolean).forEach(plugin => { - plugin(instanceRef.current.hooks) + plugin(getInstance().hooks) }) + const getUseOptionsHooks = useConsumeHookGetter( + getInstance().hooks, + 'useOptions' + ) + + // Allow useOptions hooks to modify the options coming into the table + Object.assign( + getInstance(), + reduceHooks(getUseOptionsHooks(), applyDefaults(props)) + ) + + const { + data, + columns: userColumns, + initialState, + defaultColumn, + getSubRows, + getRowId, + stateReducer, + useControlledState, + } = getInstance() + // Snapshot hook and disallow more from being added const getStateReducers = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'stateReducers' ) // Setup user reducer ref - const getUserStateReducer = useGetLatest(userStateReducer) + const getStateReducer = useGetLatest(stateReducer) // Build the reducer const reducer = React.useCallback( @@ -97,15 +118,15 @@ export const useTable = (props, ...plugins) => { return [ ...getStateReducers(), // Allow the user to add their own state reducer(s) - ...(Array.isArray(getUserStateReducer()) - ? getUserStateReducer() - : [getUserStateReducer()]), + ...(Array.isArray(getStateReducer()) + ? getStateReducer() + : [getStateReducer()]), ].reduce( (s, handler) => handler(s, action, state, instanceRef) || s, state ) }, - [getStateReducers, getUserStateReducer] + [getStateReducers, getStateReducer] ) // Start the reducer @@ -116,48 +137,49 @@ export const useTable = (props, ...plugins) => { // Allow the user to control the final state with hooks const state = useControlledState(reducerState) - Object.assign(instanceRef.current, { - state, // The state dispatcher - dispatch, // The resolved table state + Object.assign(getInstance(), { + state, + dispatch, }) // Snapshot hook and disallow more from being added - const getColumns = useConsumeHookGetter(instanceRef.current.hooks, 'columns') + const getColumnsHooks = useConsumeHookGetter(getInstance().hooks, 'columns') // Snapshot hook and disallow more from being added - const getColumnsDeps = useConsumeHookGetter( - instanceRef.current.hooks, + const getColumnsDepsHooks = useConsumeHookGetter( + getInstance().hooks, 'columnsDeps' ) // Decorate All the columns let columns = React.useMemo( () => - applyHooks( - getColumns(), + reduceHooks( + getColumnsHooks(), decorateColumnTree(userColumns, defaultColumn), - instanceRef.current + getInstance() ), [ defaultColumn, - getColumns, + getColumnsHooks, + getInstance, userColumns, // eslint-disable-next-line react-hooks/exhaustive-deps - ...getColumnsDeps(instanceRef.current), + ...getColumnsDepsHooks(getInstance()), ] ) - instanceRef.current.columns = columns + getInstance().columns = columns // Snapshot hook and disallow more from being added const getFlatColumns = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'flatColumns' ) // Snapshot hook and disallow more from being added const getFlatColumnsDeps = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'flatColumnsDeps' ) @@ -165,58 +187,60 @@ export const useTable = (props, ...plugins) => { // those columns (and trigger this memoization via deps) let flatColumns = React.useMemo( () => - applyHooks( + reduceHooks( getFlatColumns(), flattenBy(columns, 'columns'), - instanceRef.current + getInstance() ), [ columns, getFlatColumns, + getInstance, // eslint-disable-next-line react-hooks/exhaustive-deps - ...getFlatColumnsDeps(instanceRef.current), + getFlatColumnsDeps(getInstance()), ] ) - instanceRef.current.flatColumns = flatColumns + getInstance().flatColumns = flatColumns // Snapshot hook and disallow more from being added const getHeaderGroups = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'headerGroups' ) // Snapshot hook and disallow more from being added const getHeaderGroupsDeps = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'headerGroupsDeps' ) // Make the headerGroups const headerGroups = React.useMemo( () => - applyHooks( + reduceHooks( getHeaderGroups(), makeHeaderGroups(flatColumns, defaultColumn), - instanceRef.current + getInstance() ), [ defaultColumn, flatColumns, getHeaderGroups, + getInstance, // eslint-disable-next-line react-hooks/exhaustive-deps - ...getHeaderGroupsDeps(), + ...getHeaderGroupsDeps(getInstance()), ] ) - instanceRef.current.headerGroups = headerGroups + getInstance().headerGroups = headerGroups const headers = React.useMemo( () => (headerGroups.length ? headerGroups[0].headers : []), [headerGroups] ) - instanceRef.current.headers = headers + getInstance().headers = headers // Access the row model const [rows, flatRows] = React.useMemo(() => { @@ -276,54 +300,51 @@ export const useTable = (props, ...plugins) => { const accessedData = data.map((d, i) => accessRow(d, i)) return [accessedData, flatRows] - }, [data, getRowId, getSubRows, flatColumns]) + }, [data, flatColumns, getRowId, getSubRows]) - instanceRef.current.rows = rows - instanceRef.current.flatRows = flatRows + getInstance().rows = rows + getInstance().flatRows = flatRows // Provide a flat header list for utilities - instanceRef.current.flatHeaders = headerGroups.reduce( + getInstance().flatHeaders = headerGroups.reduce( (all, headerGroup) => [...all, ...headerGroup.headers], [] ) // Snapshot hook and disallow more from being added const getUseInstanceBeforeDimensions = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'useInstanceBeforeDimensions' ) - instanceRef.current = applyHooks( - getUseInstanceBeforeDimensions(), - instanceRef.current - ) + loopHooks(getUseInstanceBeforeDimensions(), getInstance()) // Header Visibility is needed by this point - calculateDimensions(instanceRef.current) + getInstance().totalColumnsWidth = calculateHeaderWidths(headers) // Snapshot hook and disallow more from being added const getUseInstance = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'useInstance' ) - instanceRef.current = applyHooks(getUseInstance(), instanceRef.current) + loopHooks(getUseInstance(), getInstance()) // Snapshot hook and disallow more from being added const getHeaderPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getHeaderProps' ) // Snapshot hook and disallow more from being added const getFooterPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getFooterProps' ) // Each materialized header needs to be assigned a render function and other // prop getter properties here. - instanceRef.current.flatHeaders.forEach(column => { + getInstance().flatHeaders.forEach(column => { // Give columns/headers rendering power column.render = (type, userProps = {}) => { const Comp = typeof type === 'string' ? column[type] : type @@ -333,48 +354,40 @@ export const useTable = (props, ...plugins) => { } return flexRender(Comp, { - ...instanceRef.current, + ...getInstance(), column, ...userProps, }) } // Give columns/headers a default getHeaderProps - column.getHeaderProps = props => - mergeProps( - { - key: ['header', column.id].join('_'), - colSpan: column.totalVisibleHeaderCount, - }, - applyPropHooks(getHeaderPropsHooks(), column, instanceRef.current), - props - ) + column.getHeaderProps = makePropGetter( + getHeaderPropsHooks(), + getInstance(), + column + ) // Give columns/headers a default getFooterProps - column.getFooterProps = props => - mergeProps( - { - key: ['footer', column.id].join('_'), - colSpan: column.totalVisibleHeaderCount, - }, - applyPropHooks(getFooterPropsHooks(), column, instanceRef.current), - props - ) + column.getFooterProps = makePropGetter( + getFooterPropsHooks(), + getInstance(), + column + ) }) // Snapshot hook and disallow more from being added const getHeaderGroupPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getHeaderGroupProps' ) // Snapshot hook and disallow more from being added - const getFooterGroupsPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + const getFooterGroupPropsHooks = useConsumeHookGetter( + getInstance().hooks, 'getFooterGroupProps' ) - instanceRef.current.headerGroups = instanceRef.current.headerGroups.filter( + getInstance().headerGroups = getInstance().headerGroups.filter( (headerGroup, i) => { // Filter out any headers and headerGroups that don't have visible columns headerGroup.headers = headerGroup.headers.filter(column => { @@ -393,31 +406,19 @@ export const useTable = (props, ...plugins) => { // Give headerGroups getRowProps if (headerGroup.headers.length) { - headerGroup.getHeaderGroupProps = (props = {}) => - mergeProps( - { - key: [`header${i}`].join('_'), - }, - applyPropHooks( - getHeaderGroupPropsHooks(), - headerGroup, - instanceRef.current - ), - props - ) + headerGroup.getHeaderGroupProps = makePropGetter( + getHeaderGroupPropsHooks(), + getInstance(), + headerGroup, + i + ) - headerGroup.getFooterGroupProps = (props = {}) => - mergeProps( - { - key: [`footer${i}`].join('_'), - }, - applyPropHooks( - getFooterGroupsPropsHooks(), - headerGroup, - instanceRef.current - ), - props - ) + headerGroup.getFooterGroupProps = makePropGetter( + getFooterGroupPropsHooks(), + getInstance(), + headerGroup, + i + ) return true } @@ -426,22 +427,17 @@ export const useTable = (props, ...plugins) => { } ) - instanceRef.current.footerGroups = [ - ...instanceRef.current.headerGroups, - ].reverse() + getInstance().footerGroups = [...getInstance().headerGroups].reverse() // Run the rows (this could be a dangerous hook with a ton of data) // Snapshot hook and disallow more from being added - const getUseRowsHooks = useConsumeHookGetter( - instanceRef.current.hooks, - 'useRows' - ) + const getUseRowsHooks = useConsumeHookGetter(getInstance().hooks, 'useRows') - instanceRef.current.rows = applyHooks( + getInstance().rows = reduceHooks( getUseRowsHooks(), - instanceRef.current.rows, - instanceRef.current + getInstance().rows, + getInstance() ) // The prepareRow function is absolutely necessary and MUST be called on @@ -449,34 +445,29 @@ export const useTable = (props, ...plugins) => { // Snapshot hook and disallow more from being added const getPrepareRowHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'prepareRow' ) // Snapshot hook and disallow more from being added const getRowPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getRowProps' ) // Snapshot hook and disallow more from being added const getCellPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getCellProps' ) - instanceRef.current.prepareRow = React.useCallback( + getInstance().prepareRow = React.useCallback( row => { - row.getRowProps = props => - mergeProps( - { key: ['row', ...row.path].join('_') }, - applyPropHooks(getRowPropsHooks(), row, instanceRef.current), - props - ) + row.getRowProps = makePropGetter(getRowPropsHooks(), getInstance(), row) // Build the visible cells for each row - row.cells = instanceRef.current.flatColumns - .filter(d => d.isVisible) + row.cells = getInstance() + .flatColumns.filter(d => d.isVisible) .map(column => { const cell = { column, @@ -485,16 +476,11 @@ export const useTable = (props, ...plugins) => { } // Give each cell a getCellProps base - cell.getCellProps = props => { - const columnPathStr = [...row.path, column.id].join('_') - return mergeProps( - { - key: ['cell', columnPathStr].join('_'), - }, - applyPropHooks(getCellPropsHooks(), cell, instanceRef.current), - props - ) - } + cell.getCellProps = makePropGetter( + getCellPropsHooks(), + getInstance(), + cell + ) // Give each cell a renderer function (supports multiple renderers) cell.render = (type, userProps = {}) => { @@ -505,7 +491,7 @@ export const useTable = (props, ...plugins) => { } return flexRender(Comp, { - ...instanceRef.current, + ...getInstance(), column, row, cell, @@ -517,42 +503,34 @@ export const useTable = (props, ...plugins) => { }) // need to apply any row specific hooks (useExpanded requires this) - applyHooks(getPrepareRowHooks(), row, instanceRef.current) + loopHooks(getPrepareRowHooks(), row, getInstance()) }, - [getCellPropsHooks, getPrepareRowHooks, getRowPropsHooks] + [getCellPropsHooks, getInstance, getPrepareRowHooks, getRowPropsHooks] ) // Snapshot hook and disallow more from being added const getTablePropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getTableProps' ) - instanceRef.current.getTableProps = userProps => - mergeProps( - applyPropHooks(getTablePropsHooks(), instanceRef.current), - userProps - ) + getInstance().getTableProps = makePropGetter( + getTablePropsHooks(), + getInstance() + ) // Snapshot hook and disallow more from being added const getTableBodyPropsHooks = useConsumeHookGetter( - instanceRef.current.hooks, + getInstance().hooks, 'getTableBodyProps' ) - instanceRef.current.getTableBodyProps = userProps => - mergeProps( - applyPropHooks(getTableBodyPropsHooks(), instanceRef.current), - userProps - ) + getInstance().getTableBodyProps = makePropGetter( + getTableBodyPropsHooks(), + getInstance() + ) - return instanceRef.current -} - -function calculateDimensions(instance) { - const { headers } = instance - - instance.totalColumnsWidth = calculateHeaderWidths(headers) + return getInstance() } function calculateHeaderWidths(headers, left = 0) { diff --git a/src/makeDefaultPluginHooks.js b/src/makeDefaultPluginHooks.js new file mode 100644 index 0000000..4212819 --- /dev/null +++ b/src/makeDefaultPluginHooks.js @@ -0,0 +1,56 @@ +const defaultGetHeaderProps = (props, instance, column) => ({ + key: ['header', column.id].join('_'), + colSpan: column.totalVisibleHeaderCount, + ...props, +}) + +const defaultGetFooterProps = (props, instance, column) => ({ + key: ['footer', column.id].join('_'), + colSpan: column.totalVisibleHeaderCount, + ...props, +}) + +const defaultGetHeaderGroupProps = (props, instance, headerGroup, index) => ({ + key: ['header', index].join('_'), + ...props, +}) + +const defaultGetFooterGroupProps = (props, instance, headerGroup, index) => ({ + key: ['footer', index].join('_'), + ...props, +}) + +const defaultGetRowProps = (props, instance, row) => ({ + key: ['row', ...row.path].join('_'), + ...props, +}) + +const defaultGetCellProps = (props, instance, cell) => ({ + ...props, + key: ['cell', ...cell.row.path, cell.column.id].join('_'), +}) + +export default function makeDefaultPluginHooks() { + return { + useOptions: [], + stateReducers: [], + columns: [], + columnsDeps: [], + flatColumns: [], + flatColumnsDeps: [], + headerGroups: [], + headerGroupsDeps: [], + useInstanceBeforeDimensions: [], + useInstance: [], + useRows: [], + prepareRow: [], + getTableProps: [], + getTableBodyProps: [], + getHeaderGroupProps: [defaultGetHeaderGroupProps], + getFooterGroupProps: [defaultGetFooterGroupProps], + getHeaderProps: [defaultGetHeaderProps], + getFooterProps: [defaultGetFooterProps], + getRowProps: [defaultGetRowProps], + getCellProps: [defaultGetCellProps], + } +} diff --git a/src/plugin-hooks/useAbsoluteLayout.js b/src/plugin-hooks/useAbsoluteLayout.js index 5b5e587..af94bb8 100644 --- a/src/plugin-hooks/useAbsoluteLayout.js +++ b/src/plugin-hooks/useAbsoluteLayout.js @@ -1,57 +1,45 @@ -export const useAbsoluteLayout = hooks => { - hooks.useInstance.push(useInstance) +// Calculating column/cells widths +const cellStyles = { + position: 'absolute', + top: 0, } -useAbsoluteLayout.pluginName = 'useAbsoluteLayout' +export const useAbsoluteLayout = hooks => { + hooks.getTableBodyProps.push(getRowStyles) + hooks.getRowProps.push(getRowStyles) + hooks.getHeaderGroupProps.push(getRowStyles) -const useInstance = instance => { - const { - totalColumnsWidth, - hooks: { - getRowProps, - getTableBodyProps, - getHeaderGroupProps, - getHeaderProps, - getCellProps, - }, - } = instance - - const rowStyles = { - style: { - position: 'relative', - width: `${totalColumnsWidth}px`, - }, - } - - getTableBodyProps.push(() => rowStyles) - getRowProps.push(() => rowStyles) - getHeaderGroupProps.push(() => rowStyles) - - // Calculating column/cells widths - const cellStyles = { - position: 'absolute', - top: 0, - } - - getHeaderProps.push(header => { - return { + hooks.getHeaderProps.push((props, instance, header) => [ + props, + { style: { ...cellStyles, left: `${header.totalLeft}px`, width: `${header.totalWidth}px`, }, - } - }) + }, + ]) - getCellProps.push(cell => { - return { + hooks.getCellProps.push((props, instance, cell) => [ + props, + { style: { ...cellStyles, left: `${cell.column.totalLeft}px`, width: `${cell.column.totalWidth}px`, }, - } - }) - - return instance + }, + ]) } + +useAbsoluteLayout.pluginName = 'useAbsoluteLayout' + +const getRowStyles = (props, instance) => [ + props, + { + style: { + position: 'relative', + width: `${instance.totalColumnsWidth}px`, + }, + }, +] diff --git a/src/plugin-hooks/useBlockLayout.js b/src/plugin-hooks/useBlockLayout.js index 970aadd..fc4f9a7 100644 --- a/src/plugin-hooks/useBlockLayout.js +++ b/src/plugin-hooks/useBlockLayout.js @@ -1,47 +1,41 @@ -export const useBlockLayout = hooks => { - hooks.useInstance.push(useInstance) +const cellStyles = { + display: 'inline-block', + boxSizing: 'border-box', } -useBlockLayout.pluginName = 'useBlockLayout' - -const useInstance = instance => { - const { - totalColumnsWidth, - hooks: { getRowProps, getHeaderGroupProps, getHeaderProps, getCellProps }, - } = instance - - const rowStyles = { +const getRowStyles = (props, instance) => [ + props, + { style: { display: 'flex', - width: `${totalColumnsWidth}px`, + width: `${instance.totalColumnsWidth}px`, }, - } + }, +] - getRowProps.push(() => rowStyles) - getHeaderGroupProps.push(() => rowStyles) +export const useBlockLayout = hooks => { + hooks.getRowProps.push(getRowStyles) + hooks.getHeaderGroupProps.push(getRowStyles) - const cellStyles = { - display: 'inline-block', - boxSizing: 'border-box', - } - - getHeaderProps.push(header => { - return { + hooks.getHeaderProps.push((props, instance, header) => [ + props, + { style: { ...cellStyles, width: `${header.totalWidth}px`, }, - } - }) + }, + ]) - getCellProps.push(cell => { - return { + hooks.getCellProps.push((props, instance, cell) => [ + props, + { style: { ...cellStyles, width: `${cell.column.totalWidth}px`, }, - } - }) - - return instance + }, + ]) } + +useBlockLayout.pluginName = 'useBlockLayout' diff --git a/src/plugin-hooks/useColumnOrder.js b/src/plugin-hooks/useColumnOrder.js index c1b7cfc..299a711 100644 --- a/src/plugin-hooks/useColumnOrder.js +++ b/src/plugin-hooks/useColumnOrder.js @@ -74,15 +74,10 @@ function flatColumns(columns, instance) { function useInstance(instance) { const { dispatch } = instance - const setColumnOrder = React.useCallback( + instance.setColumnOrder = React.useCallback( columnOrder => { return dispatch({ type: actions.setColumnOrder, columnOrder }) }, [dispatch] ) - - return { - ...instance, - setColumnOrder, - } } diff --git a/src/plugin-hooks/useExpanded.js b/src/plugin-hooks/useExpanded.js index 1537b7d..d541ba0 100755 --- a/src/plugin-hooks/useExpanded.js +++ b/src/plugin-hooks/useExpanded.js @@ -2,26 +2,39 @@ import React from 'react' import { actions, - mergeProps, - applyPropHooks, + makePropGetter, expandRows, useMountedLayoutEffect, useGetLatest, } from '../utils' +import { useConsumeHookGetter } from '../publicUtils' // Actions actions.toggleExpandedByPath = 'toggleExpandedByPath' actions.resetExpanded = 'resetExpanded' export const useExpanded = hooks => { - hooks.getExpandedToggleProps = [] - + hooks.getExpandedToggleProps = [defaultGetExpandedToggleProps] hooks.stateReducers.push(reducer) hooks.useInstance.push(useInstance) } useExpanded.pluginName = 'useExpanded' +const defaultGetExpandedToggleProps = (props, instance, row) => [ + props, + { + onClick: e => { + e.persist() + row.toggleExpanded() + }, + style: { + cursor: 'pointer', + }, + title: 'Toggle Expanded', + }, +] + // Reducer function reducer(state, action) { if (action.type === actions.init) { @@ -87,32 +100,21 @@ function useInstance(instance) { } // use reference to avoid memory leak in #1608 - const instanceRef = React.useRef() - instanceRef.current = instance + const getInstance = useGetLatest(instance) + + const getExpandedTogglePropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getExpandedToggleProps' + ) hooks.prepareRow.push(row => { - row.toggleExpanded = set => toggleExpandedByPath(row.path, set) - row.getExpandedToggleProps = props => { - return mergeProps( - { - onClick: e => { - e.persist() - row.toggleExpanded() - }, - style: { - cursor: 'pointer', - }, - title: 'Toggle Expanded', - }, - applyPropHooks( - instanceRef.current.hooks.getExpandedToggleProps, - row, - instanceRef.current - ), - props - ) - } - return row + row.toggleExpanded = set => instance.toggleExpandedByPath(row.path, set) + + row.getExpandedToggleProps = makePropGetter( + getExpandedTogglePropsHooks(), + getInstance(), + row + ) }) const expandedRows = React.useMemo(() => { @@ -125,12 +127,13 @@ function useInstance(instance) { const expandedDepth = findExpandedDepth(expanded) - return { - ...instance, + Object.assign(instance, { toggleExpandedByPath, - expandedDepth, + preExpandedRows: rows, + expandedRows, rows: expandedRows, - } + expandedDepth, + }) } function findExpandedDepth(expanded) { diff --git a/src/plugin-hooks/useFilters.js b/src/plugin-hooks/useFilters.js index 837ec33..a6cf199 100755 --- a/src/plugin-hooks/useFilters.js +++ b/src/plugin-hooks/useFilters.js @@ -117,9 +117,6 @@ function useInstance(instance) { autoResetFilters = true, } = instance - const preFilteredRows = rows - const preFilteredFlatRows = flatRows - const setFilter = (columnId, filterValue) => { dispatch({ type: actions.setFilter, columnId, filterValue }) } @@ -265,15 +262,16 @@ function useInstance(instance) { } }, [dispatch, manualFilters ? null : data]) - return { - ...instance, + Object.assign(instance, { setFilter, setAllFilters, - preFilteredRows, - preFilteredFlatRows, + preFilteredRows: rows, + preFilteredFlatRows: flatRows, + filteredRows, + filteredFlatRows, rows: filteredRows, flatRows: filteredFlatRows, - } + }) } function shouldAutoRemove(autoRemove, value) { diff --git a/src/plugin-hooks/useGroupBy.js b/src/plugin-hooks/useGroupBy.js index 849a5c8..0ddcf7a 100755 --- a/src/plugin-hooks/useGroupBy.js +++ b/src/plugin-hooks/useGroupBy.js @@ -3,20 +3,21 @@ import React from 'react' import * as aggregations from '../aggregations' import { actions, - mergeProps, - applyPropHooks, + makePropGetter, defaultGroupByFn, getFirstDefined, ensurePluginOrder, useMountedLayoutEffect, useGetLatest, } from '../utils' +import { useConsumeHookGetter } from '../publicUtils' // Actions actions.resetGroupBy = 'resetGroupBy' actions.toggleGroupBy = 'toggleGroupBy' export const useGroupBy = hooks => { + hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps] hooks.stateReducers.push(reducer) hooks.flatColumnsDeps.push((deps, instance) => [ ...deps, @@ -28,6 +29,22 @@ export const useGroupBy = hooks => { useGroupBy.pluginName = 'useGroupBy' +const defaultGetGroupByToggleProps = (props, instance, header) => [ + props, + { + onClick: header.canGroupBy + ? e => { + e.persist() + header.toggleGroupBy() + } + : undefined, + style: { + cursor: header.canGroupBy ? 'pointer' : undefined, + }, + title: 'Toggle GroupBy', + }, +] + // Reducer function reducer(state, action) { if (action.type === actions.init) { @@ -106,6 +123,8 @@ function useInstance(instance) { ensurePluginOrder(plugins, [], 'useGroupBy', ['useSortBy', 'useExpanded']) + const getInstance = useGetLatest(instance) + flatColumns.forEach(column => { const { id, @@ -135,36 +154,17 @@ function useInstance(instance) { dispatch({ type: actions.toggleGroupBy, columnId, toggle }) } - hooks.getGroupByToggleProps = [] - - // use reference to avoid memory leak in #1608 - const instanceRef = React.useRef() - instanceRef.current = instance + const getGroupByTogglePropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getGroupByToggleProps' + ) flatHeaders.forEach(header => { - const { canGroupBy } = header - header.getGroupByToggleProps = props => { - return mergeProps( - { - onClick: canGroupBy - ? e => { - e.persist() - header.toggleGroupBy() - } - : undefined, - style: { - cursor: canGroupBy ? 'pointer' : undefined, - }, - title: 'Toggle GroupBy', - }, - applyPropHooks( - instanceRef.current.hooks.getGroupByToggleProps, - header, - instanceRef.current - ), - props - ) - } + header.getGroupByToggleProps = makePropGetter( + getGroupByTogglePropsHooks(), + getInstance(), + header + ) }) hooks.prepareRow.push(row => { @@ -177,7 +177,6 @@ function useInstance(instance) { cell.isAggregated = !cell.isGrouped && !cell.isRepeatedValue && row.canExpand }) - return row }) const [groupedRows, groupedFlatRows] = React.useMemo(() => { @@ -303,11 +302,12 @@ function useInstance(instance) { } }, [dispatch, manaulGroupBy ? null : data]) - return { - ...instance, + Object.assign(instance, { + groupedRows, + groupedFlatRows, toggleGroupBy, rows: groupedRows, flatRows: groupedFlatRows, preGroupedRows: rows, - } + }) } diff --git a/src/plugin-hooks/usePagination.js b/src/plugin-hooks/usePagination.js index d0c04cd..9bf707f 100755 --- a/src/plugin-hooks/usePagination.js +++ b/src/plugin-hooks/usePagination.js @@ -168,8 +168,7 @@ function useInstance(instance) { [dispatch] ) - return { - ...instance, + Object.assign(instance, { pageOptions, pageCount, page, @@ -179,7 +178,5 @@ function useInstance(instance) { previousPage, nextPage, setPageSize, - pageIndex, - pageSize, - } + }) } diff --git a/src/plugin-hooks/useResizeColumns.js b/src/plugin-hooks/useResizeColumns.js index d2a6a35..2a3b9e8 100644 --- a/src/plugin-hooks/useResizeColumns.js +++ b/src/plugin-hooks/useResizeColumns.js @@ -2,10 +2,10 @@ import { actions, defaultColumn, getFirstDefined, - mergeProps, - applyPropHooks, + makePropGetter, useGetLatest, } from '../utils' +import { useConsumeHookGetter } from '../publicUtils' // Default Column defaultColumn.canResize = true @@ -16,10 +16,57 @@ actions.columnResizing = 'columnResizing' actions.columnDoneResizing = 'columnDoneResizing' export const useResizeColumns = hooks => { + hooks.getResizerProps = [defaultGetResizerProps] hooks.stateReducers.push(reducer) hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions) } +const defaultGetResizerProps = (props, instance, header) => { + const { dispatch } = instance + + const onMouseDown = (e, header) => { + const headersToResize = getLeafHeaders(header) + const headerIdWidths = headersToResize.map(d => [d.id, d.totalWidth]) + + const clientX = e.clientX + + const onMouseMove = e => { + const clientX = e.clientX + + dispatch({ type: actions.columnResizing, clientX }) + } + + const onMouseUp = e => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + + dispatch({ type: actions.columnDoneResizing }) + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + + dispatch({ + type: actions.columnStartResizing, + columnId: header.id, + columnWidth: header.totalWidth, + headerIdWidths, + clientX, + }) + } + + return [ + props, + { + onMouseDown: e => e.persist() || onMouseDown(e, header), + style: { + cursor: 'ew-resize', + }, + draggable: false, + }, + ] +} + useResizeColumns.pluginName = 'useResizeColumns' function reducer(state, action) { @@ -88,14 +135,11 @@ function reducer(state, action) { } const useInstanceBeforeDimensions = instance => { - instance.hooks.getResizerProps = [] - const { flatHeaders, disableResizing, hooks: { getHeaderProps }, state: { columnResizing }, - dispatch, } = instance getHeaderProps.push(() => { @@ -106,40 +150,13 @@ const useInstanceBeforeDimensions = instance => { } }) - const onMouseDown = (e, header) => { - const headersToResize = getLeafHeaders(header) - const headerIdWidths = headersToResize.map(d => [d.id, d.totalWidth]) - - const clientX = e.clientX - - const onMouseMove = e => { - const clientX = e.clientX - - dispatch({ type: actions.columnResizing, clientX }) - } - - const onMouseUp = e => { - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - - dispatch({ type: actions.columnDoneResizing }) - } - - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUp) - - dispatch({ - type: actions.columnStartResizing, - columnId: header.id, - columnWidth: header.totalWidth, - headerIdWidths, - clientX, - }) - } - - // use reference to avoid memory leak in #1608 const getInstance = useGetLatest(instance) + const getResizerPropsHooks = useConsumeHookGetter( + getInstance(), + 'getResizerProps' + ) + flatHeaders.forEach(header => { const canResize = getFirstDefined( header.disableResizing === true ? false : undefined, @@ -152,27 +169,13 @@ const useInstanceBeforeDimensions = instance => { header.isResizing = columnResizing.isResizingColumn === header.id if (canResize) { - header.getResizerProps = userProps => { - return mergeProps( - { - onMouseDown: e => e.persist() || onMouseDown(e, header), - style: { - cursor: 'ew-resize', - }, - draggable: false, - }, - applyPropHooks( - getInstance().hooks.getResizerProps, - header, - getInstance() - ), - userProps - ) - } + header.getResizerProps = makePropGetter( + getResizerPropsHooks(), + getInstance(), + header + ) } }) - - return instance } function getLeafHeaders(header) { diff --git a/src/plugin-hooks/useRowSelect.js b/src/plugin-hooks/useRowSelect.js index 40e6ba9..416bfff 100644 --- a/src/plugin-hooks/useRowSelect.js +++ b/src/plugin-hooks/useRowSelect.js @@ -2,11 +2,11 @@ import React from 'react' import { actions, - mergeProps, - applyPropHooks, + makePropGetter, ensurePluginOrder, useGetLatest, useMountedLayoutEffect, + useConsumeHookGetter, } from '../utils' const pluginName = 'useRowSelect' @@ -17,9 +17,8 @@ actions.toggleRowSelectedAll = 'toggleRowSelectedAll' actions.toggleRowSelected = 'toggleRowSelected' export const useRowSelect = hooks => { - hooks.getToggleRowSelectedProps = [] - hooks.getToggleAllRowsSelectedProps = [] - + hooks.getToggleRowSelectedProps = [defaultGetToggleRowSelectedProps] + hooks.getToggleAllRowsSelectedProps = [defaultGetToggleAllRowsSelectedProps] hooks.stateReducers.push(reducer) hooks.useRows.push(useRows) hooks.useInstance.push(useInstance) @@ -27,6 +26,45 @@ export const useRowSelect = hooks => { useRowSelect.pluginName = pluginName +const defaultGetToggleRowSelectedProps = (props, instance, row) => { + const { manualRowSelectedKey = 'isSelected' } = instance + let checked = false + + if (row.original && row.original[manualRowSelectedKey]) { + checked = true + } else { + checked = row.isSelected + } + + return [ + props, + { + onChange: e => { + row.toggleRowSelected(e.target.checked) + }, + style: { + cursor: 'pointer', + }, + checked, + title: 'Toggle Row Selected', + }, + ] +} + +const defaultGetToggleAllRowsSelectedProps = (props, instance) => [ + props, + { + onChange: e => { + instance.toggleRowSelectedAll(e.target.checked) + }, + style: { + cursor: 'pointer', + }, + checked: instance.isAllRowsSelected, + title: 'Toggle All Rows Selected', + }, +] + function reducer(state, action, previousState, instanceRef) { if (action.type === actions.init) { return { @@ -143,7 +181,6 @@ function useInstance(instance) { const { data, hooks, - manualRowSelectedKey = 'isSelected', plugins, flatRows, autoResetSelectedRows = true, @@ -183,71 +220,40 @@ function useInstance(instance) { dispatch({ type: actions.toggleRowSelected, path, selected }) // use reference to avoid memory leak in #1608 - const instanceRef = React.useRef() - instanceRef.current = instance + const getInstance = useGetLatest(instance) - const getToggleAllRowsSelectedProps = props => { - return mergeProps( - { - onChange: e => { - toggleRowSelectedAll(e.target.checked) - }, - style: { - cursor: 'pointer', - }, - checked: isAllRowsSelected, - title: 'Toggle All Rows Selected', - }, - applyPropHooks( - instanceRef.current.hooks.getToggleAllRowsSelectedProps, - instanceRef.current - ), - props - ) - } + const getToggleAllRowsSelectedPropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getToggleAllRowsSelectedProps' + ) + + const getToggleAllRowsSelectedProps = makePropGetter( + getToggleAllRowsSelectedPropsHooks(), + getInstance() + ) + + const getToggleRowSelectedPropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getToggleRowSelectedProps' + ) hooks.prepareRow.push(row => { row.toggleRowSelected = set => toggleRowSelected(row.path, set) - row.getToggleRowSelectedProps = props => { - let checked = false - if (row.original && row.original[manualRowSelectedKey]) { - checked = true - } else { - checked = row.isSelected - } - - return mergeProps( - { - onChange: e => { - row.toggleRowSelected(e.target.checked) - }, - style: { - cursor: 'pointer', - }, - checked, - title: 'Toggle Row Selected', - }, - applyPropHooks( - instanceRef.current.hooks.getToggleRowSelectedProps, - row, - instanceRef.current - ), - props - ) - } - - return row + row.getToggleRowSelectedProps = makePropGetter( + getToggleRowSelectedPropsHooks(), + getInstance(), + row + ) }) - return { - ...instance, + Object.assign(instance, { flatRowPaths, toggleRowSelected, toggleRowSelectedAll, getToggleAllRowsSelectedProps, isAllRowsSelected, - } + }) } function getRowIsSelected(row, selectedRowPaths) { diff --git a/src/plugin-hooks/useRowState.js b/src/plugin-hooks/useRowState.js index 345aae4..18f34ae 100644 --- a/src/plugin-hooks/useRowState.js +++ b/src/plugin-hooks/useRowState.js @@ -91,14 +91,6 @@ function useInstance(instance) { [setRowState] ) - const getAutoResetRowState = useGetLatest(autoResetRowState) - - useMountedLayoutEffect(() => { - if (getAutoResetRowState()) { - dispatch({ type: actions.resetRowState }) - } - }, [data]) - hooks.prepareRow.push(row => { const pathKey = row.path.join('.') @@ -120,13 +112,18 @@ function useInstance(instance) { } }) } - - return row }) - return { - ...instance, + const getAutoResetRowState = useGetLatest(autoResetRowState) + + useMountedLayoutEffect(() => { + if (getAutoResetRowState()) { + dispatch({ type: actions.resetRowState }) + } + }, [data]) + + Object.assign(instance, { setRowState, setCellState, - } + }) } diff --git a/src/plugin-hooks/useSortBy.js b/src/plugin-hooks/useSortBy.js index 4d9d367..1d39921 100755 --- a/src/plugin-hooks/useSortBy.js +++ b/src/plugin-hooks/useSortBy.js @@ -4,8 +4,8 @@ import { actions, ensurePluginOrder, defaultColumn, - mergeProps, - applyPropHooks, + makePropGetter, + useConsumeHookGetter, getFirstDefined, defaultOrderByFn, isFunction, @@ -24,12 +24,36 @@ defaultColumn.sortType = 'alphanumeric' defaultColumn.sortDescFirst = false export const useSortBy = hooks => { + hooks.getSortByToggleProps = [defaultGetSortByToggleProps] hooks.stateReducers.push(reducer) hooks.useInstance.push(useInstance) } useSortBy.pluginName = 'useSortBy' +const defaultGetSortByToggleProps = (props, instance, column) => { + const { isMultiSortEvent = e => e.shiftKey } = instance + + return [ + props, + { + onClick: column.canSort + ? e => { + e.persist() + column.toggleSortBy( + undefined, + !instance.disableMultiSort && isMultiSortEvent(e) + ) + } + : undefined, + style: { + cursor: column.canSort ? 'pointer' : undefined, + }, + title: column.canSort ? 'Toggle SortBy' : undefined, + }, + ] +} + // Reducer function reducer(state, action, previousState, instanceRef) { if (action.type === actions.init) { @@ -163,9 +187,7 @@ function useInstance(instance) { manualSortBy, defaultCanSort, disableSortBy, - isMultiSortEvent = e => e.shiftKey, flatHeaders, - hooks, state: { sortBy }, dispatch, plugins, @@ -173,8 +195,6 @@ function useInstance(instance) { } = instance ensurePluginOrder(plugins, ['useFilters'], 'useSortBy', []) - // Add custom hooks - hooks.getSortByToggleProps = [] // Updates sorting based on a columnId, desc flag and multi flag const toggleSortBy = (columnId, desc, multi) => { @@ -184,6 +204,11 @@ function useInstance(instance) { // use reference to avoid memory leak in #1608 const getInstance = useGetLatest(instance) + const getSortByTogglePropsHooks = useConsumeHookGetter( + getInstance().hooks, + 'getSortByToggleProps' + ) + // Add the getSortByToggleProps method to columns and headers flatHeaders.forEach(column => { const { @@ -212,31 +237,11 @@ function useInstance(instance) { } } - column.getSortByToggleProps = props => { - return mergeProps( - { - onClick: canSort - ? e => { - e.persist() - column.toggleSortBy( - undefined, - !getInstance().disableMultiSort && isMultiSortEvent(e) - ) - } - : undefined, - style: { - cursor: canSort ? 'pointer' : undefined, - }, - title: canSort ? 'Toggle SortBy' : undefined, - }, - applyPropHooks( - getInstance().hooks.getSortByToggleProps, - column, - getInstance() - ), - props - ) - } + column.getSortByToggleProps = makePropGetter( + getSortByTogglePropsHooks(), + getInstance(), + column + ) const columnSort = sortBy.find(d => d.id === id) column.isSorted = !!columnSort @@ -329,10 +334,10 @@ function useInstance(instance) { } }, [manualSortBy ? null : data]) - return { - ...instance, + Object.assign(instance, { toggleSortBy, - rows: sortedRows, preSortedRows: rows, - } + sortedRows, + rows: sortedRows, + }) } diff --git a/src/publicUtils.js b/src/publicUtils.js index 82702a7..987970b 100644 --- a/src/publicUtils.js +++ b/src/publicUtils.js @@ -36,41 +36,77 @@ export function defaultGroupByFn(rows, columnId) { }, {}) } -export const mergeProps = (...groups) => { - let props = {} +function mergeProps(...propList) { + return propList.reduce((props, next) => { + const { style, className, ...rest } = next - groups.forEach(({ style = {}, className, ...rest } = {}) => { props = { ...props, ...rest, style: { ...(props.style || {}), - ...style, + ...(style || {}), }, className: [props.className, className].filter(Boolean).join(' '), } - }) - if (props.className === '') { - delete props.className - } + if (props.className === '') { + delete props.className + } - return props + return props + }, {}) } -export const applyHooks = (hooks, initial, ...args) => +function handlePropGetter(prevProps, userProps, ...meta) { + // Handle a lambda, pass it the previous props + if (typeof userProps === 'function') { + return handlePropGetter({}, userProps(prevProps, ...meta)) + } + + // Handle an array, merge each item as separate props + if (Array.isArray(userProps)) { + return mergeProps(prevProps, ...userProps) + } + + // Handle an object by default, merge the two objects + return mergeProps(prevProps, userProps) +} + +export const makePropGetter = (hooks, ...meta) => { + return (userProps = {}) => + [...hooks, userProps].reduce( + (prev, next) => handlePropGetter(prev, next, ...meta), + {} + ) +} + +export const reduceHooks = (hooks, initial, ...args) => 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.' - ) + if (process.env.NODE_ENV !== 'production') { + if (typeof nextValue === 'undefined') { + console.info(next) + throw new Error( + 'React Table: A reducer hook ☝️ just returned undefined! This is not allowed.' + ) + } } return nextValue }, initial) -export const applyPropHooks = (hooks, ...args) => - hooks.reduce((prev, next) => mergeProps(prev, next(...args)), {}) +export const loopHooks = (hooks, ...args) => + hooks.forEach(hook => { + const nextValue = hook(...args) + if (process.env.NODE_ENV !== 'production') { + if (typeof nextValue !== 'undefined') { + console.info(hook, nextValue) + throw new Error( + 'React Table: A loop-type hook ☝️ just returned a value! This is not allowed.' + ) + } + } + }) export function ensurePluginOrder(plugins, befores, pluginName, afters) { const pluginIndex = plugins.findIndex( @@ -78,11 +114,13 @@ export function ensurePluginOrder(plugins, befores, pluginName, afters) { ) if (pluginIndex === -1) { - throw new Error(`The plugin ${pluginName} was not found in the plugin list! + if (process.env.NODE_ENV !== 'production') { + throw new Error(`The plugin "${pluginName}" was not found in the plugin list! This usually means you need to need to name your plugin hook by setting the 'pluginName' property of the hook function, eg: ${pluginName}.pluginName = '${pluginName}' `) + } } befores.forEach(before => { @@ -90,18 +128,22 @@ This usually means you need to need to name your plugin hook by setting the 'plu plugin => plugin.pluginName === before ) if (beforeIndex > -1 && beforeIndex > pluginIndex) { - throw new Error( - `React Table: The ${pluginName} plugin hook must be placed after the ${before} plugin hook!` - ) + if (process.env.NODE_ENV !== 'production') { + throw new Error( + `React Table: The ${pluginName} plugin hook must be placed after the ${before} plugin hook!` + ) + } } }) afters.forEach(after => { const afterIndex = plugins.findIndex(plugin => plugin.pluginName === after) - if (afterIndex > -1 && afterIndex < pluginIndex) { - throw new Error( - `React Table: The ${pluginName} plugin hook must be placed before the ${after} plugin hook!` - ) + if (process.env.NODE_ENV !== 'production') { + if (afterIndex > -1 && afterIndex < pluginIndex) { + throw new Error( + `React Table: The ${pluginName} plugin hook must be placed before the ${after} plugin hook!` + ) + } } }) }