Added/updated hooks reorganization of hooks, new hook rules

- 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> | 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<props>` - 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)
This commit is contained in:
Tanner Linsley 2019-12-10 11:35:05 -07:00
parent 9563dae006
commit 4a3311035d
21 changed files with 704 additions and 611 deletions

View File

@ -1,20 +1,20 @@
{ {
"dist/index.js": { "dist/index.js": {
"bundled": 103833, "bundled": 106450,
"minified": 48853, "minified": 50539,
"gzipped": 12789 "gzipped": 13258
}, },
"dist/index.es.js": { "dist/index.es.js": {
"bundled": 102990, "bundled": 105607,
"minified": 48103, "minified": 49789,
"gzipped": 12638, "gzipped": 13104,
"treeshaked": { "treeshaked": {
"rollup": { "rollup": {
"code": 80, "code": 80,
"import_statements": 21 "import_statements": 21
}, },
"webpack": { "webpack": {
"code": 8444 "code": 8904
} }
} }
} }

View File

@ -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> | 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<props>` - 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 ## 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. - 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.

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { useTable, mergeProps } from 'react-table' import { useTable } from 'react-table'
import makeData from './makeData' import makeData from './makeData'
@ -68,16 +68,15 @@ function Table({
<tr {...headerGroup.getHeaderGroupProps()}> <tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => ( {headerGroup.headers.map(column => (
<th <th
// Merge all of the header Props // Return an array of prop objects and react-table will merge them appropriately
{...mergeProps( {...column.getHeaderProps([
column.getHeaderProps(),
{ {
className: column.className, className: column.className,
style: column.style, style: column.style,
}, },
getColumnProps(column), getColumnProps(column),
getHeaderProps(column) getHeaderProps(column),
)} ])}
> >
{column.render('Header')} {column.render('Header')}
</th> </th>
@ -89,21 +88,20 @@ function Table({
{rows.map((row, i) => { {rows.map((row, i) => {
prepareRow(row) prepareRow(row)
return ( return (
// Merge row props // Merge user row props in
<tr {...mergeProps(row.getRowProps(), getRowProps(row))}> <tr {...row.getRowProps(getRowProps(row))}>
{row.cells.map(cell => { {row.cells.map(cell => {
return ( return (
<td <td
// Merge cell props // Return an array of prop objects and react-table will merge them appropriately
{...mergeProps( {...cell.getCellProps([
cell.getCellProps(),
{ {
className: cell.column.className, className: cell.column.className,
style: cell.column.style, style: cell.column.style,
}, },
getColumnProps(cell.column), getColumnProps(cell.column),
getCellProps(cell) getCellProps(cell),
)} ])}
> >
{cell.render('Cell')} {cell.render('Cell')}
</td> </td>

View File

@ -8218,10 +8218,10 @@ react-scripts@3.0.1:
optionalDependencies: optionalDependencies:
fsevents "2.0.6" fsevents "2.0.6"
react-table@next: react-table@latest:
version "7.0.0-beta.19" version "7.0.0-rc.6"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-beta.19.tgz#8563ae56f693cbffa10060772ba1f1cd4e926bb2" resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.6.tgz#3cb43919e31166659d6aa223bd3dfee765981f6b"
integrity sha512-BRt7zW7PGyg67fR2CK9M2fwP6BocjQN870w3P+K+vDJMkpewGjSPDscCU5sJMBqCEjKWg85k2pVkiwQPg9ofgQ== integrity sha512-iFgxMla6JnxjZwKG3eiHHOlIIANiK/HzvRFWXT8LV78N9bEOGC5A5N44S/xQ1PaZN/1c+NDBz5fBazhx/vG7/A==
react@^16.8.6: react@^16.8.6:
version "16.12.0" version "16.12.0"

View File

@ -133,9 +133,9 @@ function Table({
<tr> <tr>
{loading ? ( {loading ? (
// Use our custom loading state to show a loading indicator // Use our custom loading state to show a loading indicator
<td>Loading...</td> <td colSpan="10000">Loading...</td>
) : ( ) : (
<td> <td colSpan="10000">
Showing {page.length} of ~{controlledPageCount * pageSize}{' '} Showing {page.length} of ~{controlledPageCount * pageSize}{' '}
results results
</td> </td>

View File

@ -8087,10 +8087,10 @@ react-scripts@3.0.1:
optionalDependencies: optionalDependencies:
fsevents "2.0.6" fsevents "2.0.6"
react-table@next: react-table@latest:
version "7.0.0-alpha.7" version "7.0.0-rc.6"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-alpha.7.tgz#0cb6da6f32adb397e68505b7cdd4880d15d73017" resolved "https://registry.yarnpkg.com/react-table/-/react-table-7.0.0-rc.6.tgz#3cb43919e31166659d6aa223bd3dfee765981f6b"
integrity sha512-oXE9RRkE2CFk1OloNCSTPQ9qxOdujgkCoW5b/srbJsBog/ySkWuozBTQkxH1wGNmnSxGyTrTxJqXdXPQam7VAw== integrity sha512-iFgxMla6JnxjZwKG3eiHHOlIIANiK/HzvRFWXT8LV78N9bEOGC5A5N44S/xQ1PaZN/1c+NDBz5fBazhx/vG7/A==
react@^16.8.6: react@^16.8.6:
version "16.8.6" version "16.8.6"

View File

@ -3,9 +3,9 @@ import React from 'react'
import { import {
actions, actions,
functionalUpdate, functionalUpdate,
mergeProps,
applyPropHooks,
useGetLatest, useGetLatest,
useConsumeHookGetter,
makePropGetter,
} from '../utils' } from '../utils'
actions.resetHiddenColumns = 'resetHiddenColumns' actions.resetHiddenColumns = 'resetHiddenColumns'
@ -14,8 +14,8 @@ actions.setHiddenColumns = 'setHiddenColumns'
actions.toggleHideAllColumns = 'toggleHideAllColumns' actions.toggleHideAllColumns = 'toggleHideAllColumns'
export const useColumnVisibility = hooks => { export const useColumnVisibility = hooks => {
hooks.getToggleHiddenProps = [] hooks.getToggleHiddenProps = [defualtGetToggleHiddenProps]
hooks.getToggleHideAllColumnsProps = [] hooks.getToggleHideAllColumnsProps = [defualtGetToggleHideAllColumnsProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions) hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions)
@ -28,6 +28,36 @@ export const useColumnVisibility = hooks => {
useColumnVisibility.pluginName = 'useColumnVisibility' 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) { function reducer(state, action, previousState, instanceRef) {
if (action.type === actions.init) { if (action.type === actions.init) {
return { return {
@ -111,8 +141,6 @@ function useInstanceBeforeDimensions(instance) {
headers.forEach( headers.forEach(
subHeader => (totalVisibleHeaderCount += handleColumn(subHeader, true)) subHeader => (totalVisibleHeaderCount += handleColumn(subHeader, true))
) )
return instance
} }
function useInstance(instance) { function useInstance(instance) {
@ -127,33 +155,6 @@ function useInstance(instance) {
const allColumnsHidden = flatColumns.length === hiddenColumns.length 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( const toggleHideColumn = React.useCallback(
(columnId, value) => (columnId, value) =>
dispatch({ type: actions.toggleHideColumn, columnId, value }), dispatch({ type: actions.toggleHideColumn, columnId, value }),
@ -170,32 +171,44 @@ function useInstance(instance) {
[dispatch] [dispatch]
) )
const getToggleHideAllColumnsProps = props => { // Snapshot hook and disallow more from being added
return mergeProps( const getToggleHideAllColumnsPropsHooks = useConsumeHookGetter(
{ getInstance().hooks,
onChange: e => { 'getToggleHideAllColumnsProps'
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
) )
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,
})
} }
return { column.getToggleHiddenProps = makePropGetter(
...instance, getToggleHiddenPropsHooks(),
getInstance(),
column
)
})
Object.assign(instance, {
allColumnsHidden,
toggleHideColumn, toggleHideColumn,
setHiddenColumns, setHiddenColumns,
toggleHideAllColumns, toggleHideAllColumns,
getToggleHideAllColumnsProps, getToggleHideAllColumnsProps,
} })
} }

View File

@ -3,9 +3,9 @@ import React from 'react'
// //
import { import {
actions, actions,
applyHooks, reduceHooks,
applyPropHooks, loopHooks,
mergeProps, makePropGetter,
flexRender, flexRender,
decorateColumnTree, decorateColumnTree,
makeHeaderGroups, makeHeaderGroups,
@ -14,6 +14,8 @@ import {
useConsumeHookGetter, useConsumeHookGetter,
} from '../utils' } from '../utils'
import makeDefaultPluginHooks from '../makeDefaultPluginHooks'
import { useColumnVisibility } from './useColumnVisibility' import { useColumnVisibility } from './useColumnVisibility'
let renderErr = 'Renderer Error' let renderErr = 'Renderer Error'
@ -25,64 +27,83 @@ const defaultGetSubRows = (row, index) => row.subRows || []
const defaultGetRowId = (row, index) => index const defaultGetRowId = (row, index) => index
const defaultUseControlledState = d => d const defaultUseControlledState = d => d
export const useTable = (props, ...plugins) => { function applyDefaults(props) {
// Destructure props const {
let {
data,
columns: userColumns,
initialState = defaultInitialState, initialState = defaultInitialState,
defaultColumn = defaultColumnInstance, defaultColumn = defaultColumnInstance,
getSubRows = defaultGetSubRows, getSubRows = defaultGetSubRows,
getRowId = defaultGetRowId, getRowId = defaultGetRowId,
stateReducer: userStateReducer = defaultReducer, stateReducer = defaultReducer,
useControlledState = defaultUseControlledState, useControlledState = defaultUseControlledState,
...rest
} = props } = 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] plugins = [useColumnVisibility, ...plugins]
// The table instance // Create the table instance
let instanceRef = React.useRef({}) 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, ...props,
plugins, plugins,
data, hooks: makeDefaultPluginHooks(),
hooks: {
stateReducers: [],
columns: [],
columnsDeps: [],
flatColumns: [],
flatColumnsDeps: [],
headerGroups: [],
headerGroupsDeps: [],
useInstanceBeforeDimensions: [],
useInstance: [],
useRows: [],
prepareRow: [],
getTableProps: [],
getTableBodyProps: [],
getRowProps: [],
getHeaderGroupProps: [],
getFooterGroupProps: [],
getHeaderProps: [],
getFooterProps: [],
getCellProps: [],
},
}) })
// Allow plugins to register hooks as early as possible // Allow plugins to register hooks as early as possible
plugins.filter(Boolean).forEach(plugin => { 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 // Snapshot hook and disallow more from being added
const getStateReducers = useConsumeHookGetter( const getStateReducers = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'stateReducers' 'stateReducers'
) )
// Setup user reducer ref // Setup user reducer ref
const getUserStateReducer = useGetLatest(userStateReducer) const getStateReducer = useGetLatest(stateReducer)
// Build the reducer // Build the reducer
const reducer = React.useCallback( const reducer = React.useCallback(
@ -97,15 +118,15 @@ export const useTable = (props, ...plugins) => {
return [ return [
...getStateReducers(), ...getStateReducers(),
// Allow the user to add their own state reducer(s) // Allow the user to add their own state reducer(s)
...(Array.isArray(getUserStateReducer()) ...(Array.isArray(getStateReducer())
? getUserStateReducer() ? getStateReducer()
: [getUserStateReducer()]), : [getStateReducer()]),
].reduce( ].reduce(
(s, handler) => handler(s, action, state, instanceRef) || s, (s, handler) => handler(s, action, state, instanceRef) || s,
state state
) )
}, },
[getStateReducers, getUserStateReducer] [getStateReducers, getStateReducer]
) )
// Start the reducer // Start the reducer
@ -116,48 +137,49 @@ export const useTable = (props, ...plugins) => {
// Allow the user to control the final state with hooks // Allow the user to control the final state with hooks
const state = useControlledState(reducerState) const state = useControlledState(reducerState)
Object.assign(instanceRef.current, { Object.assign(getInstance(), {
state, // The state dispatcher state,
dispatch, // The resolved table state dispatch,
}) })
// Snapshot hook and disallow more from being added // 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 // Snapshot hook and disallow more from being added
const getColumnsDeps = useConsumeHookGetter( const getColumnsDepsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'columnsDeps' 'columnsDeps'
) )
// Decorate All the columns // Decorate All the columns
let columns = React.useMemo( let columns = React.useMemo(
() => () =>
applyHooks( reduceHooks(
getColumns(), getColumnsHooks(),
decorateColumnTree(userColumns, defaultColumn), decorateColumnTree(userColumns, defaultColumn),
instanceRef.current getInstance()
), ),
[ [
defaultColumn, defaultColumn,
getColumns, getColumnsHooks,
getInstance,
userColumns, userColumns,
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 // Snapshot hook and disallow more from being added
const getFlatColumns = useConsumeHookGetter( const getFlatColumns = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'flatColumns' 'flatColumns'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getFlatColumnsDeps = useConsumeHookGetter( const getFlatColumnsDeps = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'flatColumnsDeps' 'flatColumnsDeps'
) )
@ -165,58 +187,60 @@ export const useTable = (props, ...plugins) => {
// those columns (and trigger this memoization via deps) // those columns (and trigger this memoization via deps)
let flatColumns = React.useMemo( let flatColumns = React.useMemo(
() => () =>
applyHooks( reduceHooks(
getFlatColumns(), getFlatColumns(),
flattenBy(columns, 'columns'), flattenBy(columns, 'columns'),
instanceRef.current getInstance()
), ),
[ [
columns, columns,
getFlatColumns, getFlatColumns,
getInstance,
// eslint-disable-next-line react-hooks/exhaustive-deps // 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 // Snapshot hook and disallow more from being added
const getHeaderGroups = useConsumeHookGetter( const getHeaderGroups = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'headerGroups' 'headerGroups'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getHeaderGroupsDeps = useConsumeHookGetter( const getHeaderGroupsDeps = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'headerGroupsDeps' 'headerGroupsDeps'
) )
// Make the headerGroups // Make the headerGroups
const headerGroups = React.useMemo( const headerGroups = React.useMemo(
() => () =>
applyHooks( reduceHooks(
getHeaderGroups(), getHeaderGroups(),
makeHeaderGroups(flatColumns, defaultColumn), makeHeaderGroups(flatColumns, defaultColumn),
instanceRef.current getInstance()
), ),
[ [
defaultColumn, defaultColumn,
flatColumns, flatColumns,
getHeaderGroups, getHeaderGroups,
getInstance,
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
...getHeaderGroupsDeps(), ...getHeaderGroupsDeps(getInstance()),
] ]
) )
instanceRef.current.headerGroups = headerGroups getInstance().headerGroups = headerGroups
const headers = React.useMemo( const headers = React.useMemo(
() => (headerGroups.length ? headerGroups[0].headers : []), () => (headerGroups.length ? headerGroups[0].headers : []),
[headerGroups] [headerGroups]
) )
instanceRef.current.headers = headers getInstance().headers = headers
// Access the row model // Access the row model
const [rows, flatRows] = React.useMemo(() => { const [rows, flatRows] = React.useMemo(() => {
@ -276,54 +300,51 @@ export const useTable = (props, ...plugins) => {
const accessedData = data.map((d, i) => accessRow(d, i)) const accessedData = data.map((d, i) => accessRow(d, i))
return [accessedData, flatRows] return [accessedData, flatRows]
}, [data, getRowId, getSubRows, flatColumns]) }, [data, flatColumns, getRowId, getSubRows])
instanceRef.current.rows = rows getInstance().rows = rows
instanceRef.current.flatRows = flatRows getInstance().flatRows = flatRows
// Provide a flat header list for utilities // Provide a flat header list for utilities
instanceRef.current.flatHeaders = headerGroups.reduce( getInstance().flatHeaders = headerGroups.reduce(
(all, headerGroup) => [...all, ...headerGroup.headers], (all, headerGroup) => [...all, ...headerGroup.headers],
[] []
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getUseInstanceBeforeDimensions = useConsumeHookGetter( const getUseInstanceBeforeDimensions = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'useInstanceBeforeDimensions' 'useInstanceBeforeDimensions'
) )
instanceRef.current = applyHooks( loopHooks(getUseInstanceBeforeDimensions(), getInstance())
getUseInstanceBeforeDimensions(),
instanceRef.current
)
// Header Visibility is needed by this point // Header Visibility is needed by this point
calculateDimensions(instanceRef.current) getInstance().totalColumnsWidth = calculateHeaderWidths(headers)
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getUseInstance = useConsumeHookGetter( const getUseInstance = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'useInstance' 'useInstance'
) )
instanceRef.current = applyHooks(getUseInstance(), instanceRef.current) loopHooks(getUseInstance(), getInstance())
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getHeaderPropsHooks = useConsumeHookGetter( const getHeaderPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getHeaderProps' 'getHeaderProps'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getFooterPropsHooks = useConsumeHookGetter( const getFooterPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getFooterProps' 'getFooterProps'
) )
// Each materialized header needs to be assigned a render function and other // Each materialized header needs to be assigned a render function and other
// prop getter properties here. // prop getter properties here.
instanceRef.current.flatHeaders.forEach(column => { getInstance().flatHeaders.forEach(column => {
// Give columns/headers rendering power // Give columns/headers rendering power
column.render = (type, userProps = {}) => { column.render = (type, userProps = {}) => {
const Comp = typeof type === 'string' ? column[type] : type const Comp = typeof type === 'string' ? column[type] : type
@ -333,48 +354,40 @@ export const useTable = (props, ...plugins) => {
} }
return flexRender(Comp, { return flexRender(Comp, {
...instanceRef.current, ...getInstance(),
column, column,
...userProps, ...userProps,
}) })
} }
// Give columns/headers a default getHeaderProps // Give columns/headers a default getHeaderProps
column.getHeaderProps = props => column.getHeaderProps = makePropGetter(
mergeProps( getHeaderPropsHooks(),
{ getInstance(),
key: ['header', column.id].join('_'), column
colSpan: column.totalVisibleHeaderCount,
},
applyPropHooks(getHeaderPropsHooks(), column, instanceRef.current),
props
) )
// Give columns/headers a default getFooterProps // Give columns/headers a default getFooterProps
column.getFooterProps = props => column.getFooterProps = makePropGetter(
mergeProps( getFooterPropsHooks(),
{ getInstance(),
key: ['footer', column.id].join('_'), column
colSpan: column.totalVisibleHeaderCount,
},
applyPropHooks(getFooterPropsHooks(), column, instanceRef.current),
props
) )
}) })
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getHeaderGroupPropsHooks = useConsumeHookGetter( const getHeaderGroupPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getHeaderGroupProps' 'getHeaderGroupProps'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getFooterGroupsPropsHooks = useConsumeHookGetter( const getFooterGroupPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getFooterGroupProps' 'getFooterGroupProps'
) )
instanceRef.current.headerGroups = instanceRef.current.headerGroups.filter( getInstance().headerGroups = getInstance().headerGroups.filter(
(headerGroup, i) => { (headerGroup, i) => {
// Filter out any headers and headerGroups that don't have visible columns // Filter out any headers and headerGroups that don't have visible columns
headerGroup.headers = headerGroup.headers.filter(column => { headerGroup.headers = headerGroup.headers.filter(column => {
@ -393,30 +406,18 @@ export const useTable = (props, ...plugins) => {
// Give headerGroups getRowProps // Give headerGroups getRowProps
if (headerGroup.headers.length) { if (headerGroup.headers.length) {
headerGroup.getHeaderGroupProps = (props = {}) => headerGroup.getHeaderGroupProps = makePropGetter(
mergeProps(
{
key: [`header${i}`].join('_'),
},
applyPropHooks(
getHeaderGroupPropsHooks(), getHeaderGroupPropsHooks(),
getInstance(),
headerGroup, headerGroup,
instanceRef.current i
),
props
) )
headerGroup.getFooterGroupProps = (props = {}) => headerGroup.getFooterGroupProps = makePropGetter(
mergeProps( getFooterGroupPropsHooks(),
{ getInstance(),
key: [`footer${i}`].join('_'),
},
applyPropHooks(
getFooterGroupsPropsHooks(),
headerGroup, headerGroup,
instanceRef.current i
),
props
) )
return true return true
@ -426,22 +427,17 @@ export const useTable = (props, ...plugins) => {
} }
) )
instanceRef.current.footerGroups = [ getInstance().footerGroups = [...getInstance().headerGroups].reverse()
...instanceRef.current.headerGroups,
].reverse()
// Run the rows (this could be a dangerous hook with a ton of data) // Run the rows (this could be a dangerous hook with a ton of data)
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getUseRowsHooks = useConsumeHookGetter( const getUseRowsHooks = useConsumeHookGetter(getInstance().hooks, 'useRows')
instanceRef.current.hooks,
'useRows'
)
instanceRef.current.rows = applyHooks( getInstance().rows = reduceHooks(
getUseRowsHooks(), getUseRowsHooks(),
instanceRef.current.rows, getInstance().rows,
instanceRef.current getInstance()
) )
// The prepareRow function is absolutely necessary and MUST be called on // 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 // Snapshot hook and disallow more from being added
const getPrepareRowHooks = useConsumeHookGetter( const getPrepareRowHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'prepareRow' 'prepareRow'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getRowPropsHooks = useConsumeHookGetter( const getRowPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getRowProps' 'getRowProps'
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getCellPropsHooks = useConsumeHookGetter( const getCellPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getCellProps' 'getCellProps'
) )
instanceRef.current.prepareRow = React.useCallback( getInstance().prepareRow = React.useCallback(
row => { row => {
row.getRowProps = props => row.getRowProps = makePropGetter(getRowPropsHooks(), getInstance(), row)
mergeProps(
{ key: ['row', ...row.path].join('_') },
applyPropHooks(getRowPropsHooks(), row, instanceRef.current),
props
)
// Build the visible cells for each row // Build the visible cells for each row
row.cells = instanceRef.current.flatColumns row.cells = getInstance()
.filter(d => d.isVisible) .flatColumns.filter(d => d.isVisible)
.map(column => { .map(column => {
const cell = { const cell = {
column, column,
@ -485,16 +476,11 @@ export const useTable = (props, ...plugins) => {
} }
// Give each cell a getCellProps base // Give each cell a getCellProps base
cell.getCellProps = props => { cell.getCellProps = makePropGetter(
const columnPathStr = [...row.path, column.id].join('_') getCellPropsHooks(),
return mergeProps( getInstance(),
{ cell
key: ['cell', columnPathStr].join('_'),
},
applyPropHooks(getCellPropsHooks(), cell, instanceRef.current),
props
) )
}
// Give each cell a renderer function (supports multiple renderers) // Give each cell a renderer function (supports multiple renderers)
cell.render = (type, userProps = {}) => { cell.render = (type, userProps = {}) => {
@ -505,7 +491,7 @@ export const useTable = (props, ...plugins) => {
} }
return flexRender(Comp, { return flexRender(Comp, {
...instanceRef.current, ...getInstance(),
column, column,
row, row,
cell, cell,
@ -517,42 +503,34 @@ export const useTable = (props, ...plugins) => {
}) })
// need to apply any row specific hooks (useExpanded requires this) // 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 // Snapshot hook and disallow more from being added
const getTablePropsHooks = useConsumeHookGetter( const getTablePropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getTableProps' 'getTableProps'
) )
instanceRef.current.getTableProps = userProps => getInstance().getTableProps = makePropGetter(
mergeProps( getTablePropsHooks(),
applyPropHooks(getTablePropsHooks(), instanceRef.current), getInstance()
userProps
) )
// Snapshot hook and disallow more from being added // Snapshot hook and disallow more from being added
const getTableBodyPropsHooks = useConsumeHookGetter( const getTableBodyPropsHooks = useConsumeHookGetter(
instanceRef.current.hooks, getInstance().hooks,
'getTableBodyProps' 'getTableBodyProps'
) )
instanceRef.current.getTableBodyProps = userProps => getInstance().getTableBodyProps = makePropGetter(
mergeProps( getTableBodyPropsHooks(),
applyPropHooks(getTableBodyPropsHooks(), instanceRef.current), getInstance()
userProps
) )
return instanceRef.current return getInstance()
}
function calculateDimensions(instance) {
const { headers } = instance
instance.totalColumnsWidth = calculateHeaderWidths(headers)
} }
function calculateHeaderWidths(headers, left = 0) { function calculateHeaderWidths(headers, left = 0) {

View File

@ -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],
}
}

View File

@ -1,57 +1,45 @@
export const useAbsoluteLayout = hooks => { // Calculating column/cells widths
hooks.useInstance.push(useInstance) const cellStyles = {
}
useAbsoluteLayout.pluginName = 'useAbsoluteLayout'
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', position: 'absolute',
top: 0, top: 0,
} }
getHeaderProps.push(header => { export const useAbsoluteLayout = hooks => {
return { hooks.getTableBodyProps.push(getRowStyles)
hooks.getRowProps.push(getRowStyles)
hooks.getHeaderGroupProps.push(getRowStyles)
hooks.getHeaderProps.push((props, instance, header) => [
props,
{
style: { style: {
...cellStyles, ...cellStyles,
left: `${header.totalLeft}px`, left: `${header.totalLeft}px`,
width: `${header.totalWidth}px`, width: `${header.totalWidth}px`,
}, },
} },
}) ])
getCellProps.push(cell => { hooks.getCellProps.push((props, instance, cell) => [
return { props,
{
style: { style: {
...cellStyles, ...cellStyles,
left: `${cell.column.totalLeft}px`, left: `${cell.column.totalLeft}px`,
width: `${cell.column.totalWidth}px`, width: `${cell.column.totalWidth}px`,
}, },
} },
}) ])
return instance
} }
useAbsoluteLayout.pluginName = 'useAbsoluteLayout'
const getRowStyles = (props, instance) => [
props,
{
style: {
position: 'relative',
width: `${instance.totalColumnsWidth}px`,
},
},
]

View File

@ -1,47 +1,41 @@
export const useBlockLayout = hooks => { const cellStyles = {
hooks.useInstance.push(useInstance)
}
useBlockLayout.pluginName = 'useBlockLayout'
const useInstance = instance => {
const {
totalColumnsWidth,
hooks: { getRowProps, getHeaderGroupProps, getHeaderProps, getCellProps },
} = instance
const rowStyles = {
style: {
display: 'flex',
width: `${totalColumnsWidth}px`,
},
}
getRowProps.push(() => rowStyles)
getHeaderGroupProps.push(() => rowStyles)
const cellStyles = {
display: 'inline-block', display: 'inline-block',
boxSizing: 'border-box', boxSizing: 'border-box',
} }
getHeaderProps.push(header => { const getRowStyles = (props, instance) => [
return { props,
{
style: {
display: 'flex',
width: `${instance.totalColumnsWidth}px`,
},
},
]
export const useBlockLayout = hooks => {
hooks.getRowProps.push(getRowStyles)
hooks.getHeaderGroupProps.push(getRowStyles)
hooks.getHeaderProps.push((props, instance, header) => [
props,
{
style: { style: {
...cellStyles, ...cellStyles,
width: `${header.totalWidth}px`, width: `${header.totalWidth}px`,
}, },
} },
}) ])
getCellProps.push(cell => { hooks.getCellProps.push((props, instance, cell) => [
return { props,
{
style: { style: {
...cellStyles, ...cellStyles,
width: `${cell.column.totalWidth}px`, width: `${cell.column.totalWidth}px`,
}, },
} },
}) ])
return instance
} }
useBlockLayout.pluginName = 'useBlockLayout'

View File

@ -74,15 +74,10 @@ function flatColumns(columns, instance) {
function useInstance(instance) { function useInstance(instance) {
const { dispatch } = instance const { dispatch } = instance
const setColumnOrder = React.useCallback( instance.setColumnOrder = React.useCallback(
columnOrder => { columnOrder => {
return dispatch({ type: actions.setColumnOrder, columnOrder }) return dispatch({ type: actions.setColumnOrder, columnOrder })
}, },
[dispatch] [dispatch]
) )
return {
...instance,
setColumnOrder,
}
} }

View File

@ -2,26 +2,39 @@ import React from 'react'
import { import {
actions, actions,
mergeProps, makePropGetter,
applyPropHooks,
expandRows, expandRows,
useMountedLayoutEffect, useMountedLayoutEffect,
useGetLatest, useGetLatest,
} from '../utils' } from '../utils'
import { useConsumeHookGetter } from '../publicUtils'
// Actions // Actions
actions.toggleExpandedByPath = 'toggleExpandedByPath' actions.toggleExpandedByPath = 'toggleExpandedByPath'
actions.resetExpanded = 'resetExpanded' actions.resetExpanded = 'resetExpanded'
export const useExpanded = hooks => { export const useExpanded = hooks => {
hooks.getExpandedToggleProps = [] hooks.getExpandedToggleProps = [defaultGetExpandedToggleProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance) hooks.useInstance.push(useInstance)
} }
useExpanded.pluginName = 'useExpanded' useExpanded.pluginName = 'useExpanded'
const defaultGetExpandedToggleProps = (props, instance, row) => [
props,
{
onClick: e => {
e.persist()
row.toggleExpanded()
},
style: {
cursor: 'pointer',
},
title: 'Toggle Expanded',
},
]
// Reducer // Reducer
function reducer(state, action) { function reducer(state, action) {
if (action.type === actions.init) { if (action.type === actions.init) {
@ -87,32 +100,21 @@ function useInstance(instance) {
} }
// use reference to avoid memory leak in #1608 // use reference to avoid memory leak in #1608
const instanceRef = React.useRef() const getInstance = useGetLatest(instance)
instanceRef.current = instance
const getExpandedTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getExpandedToggleProps'
)
hooks.prepareRow.push(row => { hooks.prepareRow.push(row => {
row.toggleExpanded = set => toggleExpandedByPath(row.path, set) row.toggleExpanded = set => instance.toggleExpandedByPath(row.path, set)
row.getExpandedToggleProps = props => {
return mergeProps( row.getExpandedToggleProps = makePropGetter(
{ getExpandedTogglePropsHooks(),
onClick: e => { getInstance(),
e.persist() row
row.toggleExpanded()
},
style: {
cursor: 'pointer',
},
title: 'Toggle Expanded',
},
applyPropHooks(
instanceRef.current.hooks.getExpandedToggleProps,
row,
instanceRef.current
),
props
) )
}
return row
}) })
const expandedRows = React.useMemo(() => { const expandedRows = React.useMemo(() => {
@ -125,12 +127,13 @@ function useInstance(instance) {
const expandedDepth = findExpandedDepth(expanded) const expandedDepth = findExpandedDepth(expanded)
return { Object.assign(instance, {
...instance,
toggleExpandedByPath, toggleExpandedByPath,
expandedDepth, preExpandedRows: rows,
expandedRows,
rows: expandedRows, rows: expandedRows,
} expandedDepth,
})
} }
function findExpandedDepth(expanded) { function findExpandedDepth(expanded) {

View File

@ -117,9 +117,6 @@ function useInstance(instance) {
autoResetFilters = true, autoResetFilters = true,
} = instance } = instance
const preFilteredRows = rows
const preFilteredFlatRows = flatRows
const setFilter = (columnId, filterValue) => { const setFilter = (columnId, filterValue) => {
dispatch({ type: actions.setFilter, columnId, filterValue }) dispatch({ type: actions.setFilter, columnId, filterValue })
} }
@ -265,15 +262,16 @@ function useInstance(instance) {
} }
}, [dispatch, manualFilters ? null : data]) }, [dispatch, manualFilters ? null : data])
return { Object.assign(instance, {
...instance,
setFilter, setFilter,
setAllFilters, setAllFilters,
preFilteredRows, preFilteredRows: rows,
preFilteredFlatRows, preFilteredFlatRows: flatRows,
filteredRows,
filteredFlatRows,
rows: filteredRows, rows: filteredRows,
flatRows: filteredFlatRows, flatRows: filteredFlatRows,
} })
} }
function shouldAutoRemove(autoRemove, value) { function shouldAutoRemove(autoRemove, value) {

View File

@ -3,20 +3,21 @@ import React from 'react'
import * as aggregations from '../aggregations' import * as aggregations from '../aggregations'
import { import {
actions, actions,
mergeProps, makePropGetter,
applyPropHooks,
defaultGroupByFn, defaultGroupByFn,
getFirstDefined, getFirstDefined,
ensurePluginOrder, ensurePluginOrder,
useMountedLayoutEffect, useMountedLayoutEffect,
useGetLatest, useGetLatest,
} from '../utils' } from '../utils'
import { useConsumeHookGetter } from '../publicUtils'
// Actions // Actions
actions.resetGroupBy = 'resetGroupBy' actions.resetGroupBy = 'resetGroupBy'
actions.toggleGroupBy = 'toggleGroupBy' actions.toggleGroupBy = 'toggleGroupBy'
export const useGroupBy = hooks => { export const useGroupBy = hooks => {
hooks.getGroupByToggleProps = [defaultGetGroupByToggleProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.flatColumnsDeps.push((deps, instance) => [ hooks.flatColumnsDeps.push((deps, instance) => [
...deps, ...deps,
@ -28,6 +29,22 @@ export const useGroupBy = hooks => {
useGroupBy.pluginName = 'useGroupBy' 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 // Reducer
function reducer(state, action) { function reducer(state, action) {
if (action.type === actions.init) { if (action.type === actions.init) {
@ -106,6 +123,8 @@ function useInstance(instance) {
ensurePluginOrder(plugins, [], 'useGroupBy', ['useSortBy', 'useExpanded']) ensurePluginOrder(plugins, [], 'useGroupBy', ['useSortBy', 'useExpanded'])
const getInstance = useGetLatest(instance)
flatColumns.forEach(column => { flatColumns.forEach(column => {
const { const {
id, id,
@ -135,36 +154,17 @@ function useInstance(instance) {
dispatch({ type: actions.toggleGroupBy, columnId, toggle }) dispatch({ type: actions.toggleGroupBy, columnId, toggle })
} }
hooks.getGroupByToggleProps = [] const getGroupByTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
// use reference to avoid memory leak in #1608 'getGroupByToggleProps'
const instanceRef = React.useRef() )
instanceRef.current = instance
flatHeaders.forEach(header => { flatHeaders.forEach(header => {
const { canGroupBy } = header header.getGroupByToggleProps = makePropGetter(
header.getGroupByToggleProps = props => { getGroupByTogglePropsHooks(),
return mergeProps( getInstance(),
{ header
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
) )
}
}) })
hooks.prepareRow.push(row => { hooks.prepareRow.push(row => {
@ -177,7 +177,6 @@ function useInstance(instance) {
cell.isAggregated = cell.isAggregated =
!cell.isGrouped && !cell.isRepeatedValue && row.canExpand !cell.isGrouped && !cell.isRepeatedValue && row.canExpand
}) })
return row
}) })
const [groupedRows, groupedFlatRows] = React.useMemo(() => { const [groupedRows, groupedFlatRows] = React.useMemo(() => {
@ -303,11 +302,12 @@ function useInstance(instance) {
} }
}, [dispatch, manaulGroupBy ? null : data]) }, [dispatch, manaulGroupBy ? null : data])
return { Object.assign(instance, {
...instance, groupedRows,
groupedFlatRows,
toggleGroupBy, toggleGroupBy,
rows: groupedRows, rows: groupedRows,
flatRows: groupedFlatRows, flatRows: groupedFlatRows,
preGroupedRows: rows, preGroupedRows: rows,
} })
} }

View File

@ -168,8 +168,7 @@ function useInstance(instance) {
[dispatch] [dispatch]
) )
return { Object.assign(instance, {
...instance,
pageOptions, pageOptions,
pageCount, pageCount,
page, page,
@ -179,7 +178,5 @@ function useInstance(instance) {
previousPage, previousPage,
nextPage, nextPage,
setPageSize, setPageSize,
pageIndex, })
pageSize,
}
} }

View File

@ -2,10 +2,10 @@ import {
actions, actions,
defaultColumn, defaultColumn,
getFirstDefined, getFirstDefined,
mergeProps, makePropGetter,
applyPropHooks,
useGetLatest, useGetLatest,
} from '../utils' } from '../utils'
import { useConsumeHookGetter } from '../publicUtils'
// Default Column // Default Column
defaultColumn.canResize = true defaultColumn.canResize = true
@ -16,10 +16,57 @@ actions.columnResizing = 'columnResizing'
actions.columnDoneResizing = 'columnDoneResizing' actions.columnDoneResizing = 'columnDoneResizing'
export const useResizeColumns = hooks => { export const useResizeColumns = hooks => {
hooks.getResizerProps = [defaultGetResizerProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.useInstanceBeforeDimensions.push(useInstanceBeforeDimensions) 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' useResizeColumns.pluginName = 'useResizeColumns'
function reducer(state, action) { function reducer(state, action) {
@ -88,14 +135,11 @@ function reducer(state, action) {
} }
const useInstanceBeforeDimensions = instance => { const useInstanceBeforeDimensions = instance => {
instance.hooks.getResizerProps = []
const { const {
flatHeaders, flatHeaders,
disableResizing, disableResizing,
hooks: { getHeaderProps }, hooks: { getHeaderProps },
state: { columnResizing }, state: { columnResizing },
dispatch,
} = instance } = instance
getHeaderProps.push(() => { 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 getInstance = useGetLatest(instance)
const getResizerPropsHooks = useConsumeHookGetter(
getInstance(),
'getResizerProps'
)
flatHeaders.forEach(header => { flatHeaders.forEach(header => {
const canResize = getFirstDefined( const canResize = getFirstDefined(
header.disableResizing === true ? false : undefined, header.disableResizing === true ? false : undefined,
@ -152,27 +169,13 @@ const useInstanceBeforeDimensions = instance => {
header.isResizing = columnResizing.isResizingColumn === header.id header.isResizing = columnResizing.isResizingColumn === header.id
if (canResize) { if (canResize) {
header.getResizerProps = userProps => { header.getResizerProps = makePropGetter(
return mergeProps( getResizerPropsHooks(),
{ getInstance(),
onMouseDown: e => e.persist() || onMouseDown(e, header), header
style: {
cursor: 'ew-resize',
},
draggable: false,
},
applyPropHooks(
getInstance().hooks.getResizerProps,
header,
getInstance()
),
userProps
) )
} }
}
}) })
return instance
} }
function getLeafHeaders(header) { function getLeafHeaders(header) {

View File

@ -2,11 +2,11 @@ import React from 'react'
import { import {
actions, actions,
mergeProps, makePropGetter,
applyPropHooks,
ensurePluginOrder, ensurePluginOrder,
useGetLatest, useGetLatest,
useMountedLayoutEffect, useMountedLayoutEffect,
useConsumeHookGetter,
} from '../utils' } from '../utils'
const pluginName = 'useRowSelect' const pluginName = 'useRowSelect'
@ -17,9 +17,8 @@ actions.toggleRowSelectedAll = 'toggleRowSelectedAll'
actions.toggleRowSelected = 'toggleRowSelected' actions.toggleRowSelected = 'toggleRowSelected'
export const useRowSelect = hooks => { export const useRowSelect = hooks => {
hooks.getToggleRowSelectedProps = [] hooks.getToggleRowSelectedProps = [defaultGetToggleRowSelectedProps]
hooks.getToggleAllRowsSelectedProps = [] hooks.getToggleAllRowsSelectedProps = [defaultGetToggleAllRowsSelectedProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.useRows.push(useRows) hooks.useRows.push(useRows)
hooks.useInstance.push(useInstance) hooks.useInstance.push(useInstance)
@ -27,6 +26,45 @@ export const useRowSelect = hooks => {
useRowSelect.pluginName = pluginName 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) { function reducer(state, action, previousState, instanceRef) {
if (action.type === actions.init) { if (action.type === actions.init) {
return { return {
@ -143,7 +181,6 @@ function useInstance(instance) {
const { const {
data, data,
hooks, hooks,
manualRowSelectedKey = 'isSelected',
plugins, plugins,
flatRows, flatRows,
autoResetSelectedRows = true, autoResetSelectedRows = true,
@ -183,71 +220,40 @@ function useInstance(instance) {
dispatch({ type: actions.toggleRowSelected, path, selected }) dispatch({ type: actions.toggleRowSelected, path, selected })
// use reference to avoid memory leak in #1608 // use reference to avoid memory leak in #1608
const instanceRef = React.useRef() const getInstance = useGetLatest(instance)
instanceRef.current = instance
const getToggleAllRowsSelectedProps = props => { const getToggleAllRowsSelectedPropsHooks = useConsumeHookGetter(
return mergeProps( getInstance().hooks,
{ 'getToggleAllRowsSelectedProps'
onChange: e => { )
toggleRowSelectedAll(e.target.checked)
}, const getToggleAllRowsSelectedProps = makePropGetter(
style: { getToggleAllRowsSelectedPropsHooks(),
cursor: 'pointer', getInstance()
}, )
checked: isAllRowsSelected,
title: 'Toggle All Rows Selected', const getToggleRowSelectedPropsHooks = useConsumeHookGetter(
}, getInstance().hooks,
applyPropHooks( 'getToggleRowSelectedProps'
instanceRef.current.hooks.getToggleAllRowsSelectedProps,
instanceRef.current
),
props
) )
}
hooks.prepareRow.push(row => { hooks.prepareRow.push(row => {
row.toggleRowSelected = set => toggleRowSelected(row.path, set) row.toggleRowSelected = set => toggleRowSelected(row.path, set)
row.getToggleRowSelectedProps = props => {
let checked = false
if (row.original && row.original[manualRowSelectedKey]) { row.getToggleRowSelectedProps = makePropGetter(
checked = true getToggleRowSelectedPropsHooks(),
} else { getInstance(),
checked = row.isSelected row
}
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
}) })
return { Object.assign(instance, {
...instance,
flatRowPaths, flatRowPaths,
toggleRowSelected, toggleRowSelected,
toggleRowSelectedAll, toggleRowSelectedAll,
getToggleAllRowsSelectedProps, getToggleAllRowsSelectedProps,
isAllRowsSelected, isAllRowsSelected,
} })
} }
function getRowIsSelected(row, selectedRowPaths) { function getRowIsSelected(row, selectedRowPaths) {

View File

@ -91,14 +91,6 @@ function useInstance(instance) {
[setRowState] [setRowState]
) )
const getAutoResetRowState = useGetLatest(autoResetRowState)
useMountedLayoutEffect(() => {
if (getAutoResetRowState()) {
dispatch({ type: actions.resetRowState })
}
}, [data])
hooks.prepareRow.push(row => { hooks.prepareRow.push(row => {
const pathKey = row.path.join('.') const pathKey = row.path.join('.')
@ -120,13 +112,18 @@ function useInstance(instance) {
} }
}) })
} }
return row
}) })
return { const getAutoResetRowState = useGetLatest(autoResetRowState)
...instance,
useMountedLayoutEffect(() => {
if (getAutoResetRowState()) {
dispatch({ type: actions.resetRowState })
}
}, [data])
Object.assign(instance, {
setRowState, setRowState,
setCellState, setCellState,
} })
} }

View File

@ -4,8 +4,8 @@ import {
actions, actions,
ensurePluginOrder, ensurePluginOrder,
defaultColumn, defaultColumn,
mergeProps, makePropGetter,
applyPropHooks, useConsumeHookGetter,
getFirstDefined, getFirstDefined,
defaultOrderByFn, defaultOrderByFn,
isFunction, isFunction,
@ -24,12 +24,36 @@ defaultColumn.sortType = 'alphanumeric'
defaultColumn.sortDescFirst = false defaultColumn.sortDescFirst = false
export const useSortBy = hooks => { export const useSortBy = hooks => {
hooks.getSortByToggleProps = [defaultGetSortByToggleProps]
hooks.stateReducers.push(reducer) hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance) hooks.useInstance.push(useInstance)
} }
useSortBy.pluginName = 'useSortBy' 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 // Reducer
function reducer(state, action, previousState, instanceRef) { function reducer(state, action, previousState, instanceRef) {
if (action.type === actions.init) { if (action.type === actions.init) {
@ -163,9 +187,7 @@ function useInstance(instance) {
manualSortBy, manualSortBy,
defaultCanSort, defaultCanSort,
disableSortBy, disableSortBy,
isMultiSortEvent = e => e.shiftKey,
flatHeaders, flatHeaders,
hooks,
state: { sortBy }, state: { sortBy },
dispatch, dispatch,
plugins, plugins,
@ -173,8 +195,6 @@ function useInstance(instance) {
} = instance } = instance
ensurePluginOrder(plugins, ['useFilters'], 'useSortBy', []) ensurePluginOrder(plugins, ['useFilters'], 'useSortBy', [])
// Add custom hooks
hooks.getSortByToggleProps = []
// Updates sorting based on a columnId, desc flag and multi flag // Updates sorting based on a columnId, desc flag and multi flag
const toggleSortBy = (columnId, desc, multi) => { const toggleSortBy = (columnId, desc, multi) => {
@ -184,6 +204,11 @@ function useInstance(instance) {
// use reference to avoid memory leak in #1608 // use reference to avoid memory leak in #1608
const getInstance = useGetLatest(instance) const getInstance = useGetLatest(instance)
const getSortByTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getSortByToggleProps'
)
// Add the getSortByToggleProps method to columns and headers // Add the getSortByToggleProps method to columns and headers
flatHeaders.forEach(column => { flatHeaders.forEach(column => {
const { const {
@ -212,31 +237,11 @@ function useInstance(instance) {
} }
} }
column.getSortByToggleProps = props => { column.getSortByToggleProps = makePropGetter(
return mergeProps( getSortByTogglePropsHooks(),
{ getInstance(),
onClick: canSort column
? 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
)
}
const columnSort = sortBy.find(d => d.id === id) const columnSort = sortBy.find(d => d.id === id)
column.isSorted = !!columnSort column.isSorted = !!columnSort
@ -329,10 +334,10 @@ function useInstance(instance) {
} }
}, [manualSortBy ? null : data]) }, [manualSortBy ? null : data])
return { Object.assign(instance, {
...instance,
toggleSortBy, toggleSortBy,
rows: sortedRows,
preSortedRows: rows, preSortedRows: rows,
} sortedRows,
rows: sortedRows,
})
} }

View File

@ -36,41 +36,77 @@ export function defaultGroupByFn(rows, columnId) {
}, {}) }, {})
} }
export const mergeProps = (...groups) => { function mergeProps(...propList) {
let props = {} return propList.reduce((props, next) => {
const { style, className, ...rest } = next
groups.forEach(({ style = {}, className, ...rest } = {}) => {
props = { props = {
...props, ...props,
...rest, ...rest,
style: { style: {
...(props.style || {}), ...(props.style || {}),
...style, ...(style || {}),
}, },
className: [props.className, className].filter(Boolean).join(' '), className: [props.className, className].filter(Boolean).join(' '),
} }
})
if (props.className === '') { if (props.className === '') {
delete 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) => { hooks.reduce((prev, next) => {
const nextValue = next(prev, ...args) const nextValue = next(prev, ...args)
if (process.env.NODE_ENV !== 'production') {
if (typeof nextValue === 'undefined') { if (typeof nextValue === 'undefined') {
console.info(next)
throw new Error( throw new Error(
'React Table: A hook just returned undefined! This is not allowed.' 'React Table: A reducer hook ☝️ just returned undefined! This is not allowed.'
) )
} }
}
return nextValue return nextValue
}, initial) }, initial)
export const applyPropHooks = (hooks, ...args) => export const loopHooks = (hooks, ...args) =>
hooks.reduce((prev, next) => mergeProps(prev, next(...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) { export function ensurePluginOrder(plugins, befores, pluginName, afters) {
const pluginIndex = plugins.findIndex( const pluginIndex = plugins.findIndex(
@ -78,31 +114,37 @@ export function ensurePluginOrder(plugins, befores, pluginName, afters) {
) )
if (pluginIndex === -1) { 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: 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}' ${pluginName}.pluginName = '${pluginName}'
`) `)
} }
}
befores.forEach(before => { befores.forEach(before => {
const beforeIndex = plugins.findIndex( const beforeIndex = plugins.findIndex(
plugin => plugin.pluginName === before plugin => plugin.pluginName === before
) )
if (beforeIndex > -1 && beforeIndex > pluginIndex) { if (beforeIndex > -1 && beforeIndex > pluginIndex) {
if (process.env.NODE_ENV !== 'production') {
throw new Error( throw new Error(
`React Table: The ${pluginName} plugin hook must be placed after the ${before} plugin hook!` `React Table: The ${pluginName} plugin hook must be placed after the ${before} plugin hook!`
) )
} }
}
}) })
afters.forEach(after => { afters.forEach(after => {
const afterIndex = plugins.findIndex(plugin => plugin.pluginName === after) const afterIndex = plugins.findIndex(plugin => plugin.pluginName === after)
if (process.env.NODE_ENV !== 'production') {
if (afterIndex > -1 && afterIndex < pluginIndex) { if (afterIndex > -1 && afterIndex < pluginIndex) {
throw new Error( throw new Error(
`React Table: The ${pluginName} plugin hook must be placed before the ${after} plugin hook!` `React Table: The ${pluginName} plugin hook must be placed before the ${after} plugin hook!`
) )
} }
}
}) })
} }