I am rendering a table with the react-table library in its version 7.6.2.
The table (is a paginated table) has the functionality to add or delete rows, as well as edit cell values. Each time the user adds a new row or edits a cell, the cell or row is updated with a blue background.
Up to this point, everything works correctly. The problem comes when deleting a row. After deleting the row from the data structure, the row is removed from the table, but the color of the row remains in the table until the pagination is updated.
My production app works with redux, but I've created a simplified sandbox to reproduce the bug.
I have validated that tableData is updated correctly.
import React, { useState, useEffect, useMemo } from 'react'
import PropTypes from 'prop-types'
import Notifier from 'components/common/Notifier'
import ContextMenu from './ContextMenu'
import CustomTable, { header } from './customTable'
import colorSelector from './coloring'
const SequenceTable = ({ tableData = [], sTool = null, sPart = null, onRowSelect = () => {}, onUpdateTableData = () => {}, handlePartChange = () => {} }) => {
const columns = useMemo(() => header, [])
const [skipPageReset, setSkipPageReset] = useState(false)
const [selectedRow, setSelectedRow] = useState(null)
const [mousePos, setMousePos] = useState({ x: null, y: null })
const [contextRow, setContextRow] = useState(null)
const updateMyData = (rowIndex, columnId, value) => {
setSkipPageReset(true)
onUpdateTableData({ rowIndex: rowIndex, columnId: columnId, value: value })
}
const handleContextMenuOpen = (event, row) => {
event.preventDefault()
setMousePos({ x: event.clientX, y: event.clientY })
setContextRow(row.values)
}
const handleContextMenuClose = () => {
setContextRow(null)
setMousePos({ x: null, y: null })
}
useEffect(() => {
onRowSelect(selectedRow)
}, [selectedRow])
useEffect(() => {
if (tableData != null && tableData.length !== 0) handlePartChange(sTool, tableData[0])
}, [sPart])
useEffect(() => setSkipPageReset(false), [sTool, sPart])
return (
<React.Fragment>
<CustomTable
columns={columns}
data={tableData}
updateMyData={updateMyData}
openContextMenu={handleContextMenuOpen}
setSelectedRow={setSelectedRow}
skipPageReset={skipPageReset}
getCellProps={cellInfo => colorSelector(cellInfo.value ? cellInfo.value.colorCode : -1)}
/>
<ContextMenu mousePos={mousePos} row={contextRow} onClose={() => handleContextMenuClose()} />
<Notifier />
</React.Fragment>
)
}
SequenceTable.propTypes = {
tableData: PropTypes.array,
sTool: PropTypes.string,
sPart: PropTypes.string
}
export default SequenceTable
import React, { useEffect } from 'react'
import { useTable, usePagination, useSortBy, useRowSelect } from 'react-table'
import Table from 'react-bootstrap/Table'
import ClickAndHold from 'components/common/ClickAndHold'
import EditableCell from './EditableCell'
import Pagination from './Pagination'
const defaultColumn = { Cell: EditableCell }
const CustomTable = ({ columns, data, updateMyData, openContextMenu, setSelectedRow, skipPageReset, getCellProps = () => ({}) }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
selectedFlatRows,
state: { pageIndex, pageSize, selectedRowIds }
} = useTable(
{
columns,
data,
stateReducer: (newState, action) => {
if (action.type === 'toggleRowSelected') {
newState.selectedRowIds = {
[action.id]: true
}
}
return newState
},
defaultColumn,
autoResetPage: !skipPageReset,
updateMyData,
initialState: {
sortBy: [
{
id: 'id',
desc: false
}
],
hiddenColumns: ['id']
}
},
useSortBy,
usePagination,
useRowSelect
)
useEffect(() => {
if (selectedFlatRows.length !== 0) setSelectedRow(selectedFlatRows[0].original)
}, [setSelectedRow, selectedRowIds])
return (
<React.Fragment>
<Table responsive striped bordered hover size="sm" {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row)
return (
<ClickAndHold id={i} elmType={'tr'} onHold={e => openContextMenu(e, row)} {...row.getRowProps()} onContextMenu={e => openContextMenu(e, row)}>
{row.cells.map(cell => {
return <td {...cell.getCellProps([getCellProps(cell)])}>{cell.render('Cell')}</td>
})}
</ClickAndHold>
)
})}
</tbody>
</Table>
<Pagination
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
pageOption={pageOptions}
pageCount={pageCount}
gotoPage={gotoPage}
nextPage={nextPage}
previousPage={previousPage}
setPageSize={setPageSize}
pageIndex={pageIndex}
pageSize={pageSize}
/>
</React.Fragment>
)
}
export default CustomTable
My custom cell component:
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import InputBase from '@material-ui/core/InputBase'
import { openSnackbar } from 'components/common/Notifier'
const EditableCell = (
{
value: initialValue,
row: { index },
column: { id },
updateMyData // This is a custom function that we supplied to our table instance
},
{ literal = () => '' }
) => {
const [isValid, setIsValid] = useState(true)
const [value, setValue] = useState(initialValue)
const [errorMsg, setErrorMsg] = useState('')
const [edited, setEdited] = useState(false)
const onChange = e => {
e.persist()
setEdited(true)
let valid = true
if (value.type === 'bool' && e.target.value !== 'true' && e.target.value !== 'false') {
console.log('mustBeBoolean')
valid = false
}
if (value.type === 'number' && isNaN(e.target.value)) {
console.log('mustBeNumeric')
valid = false
}
setValue(oldVal => {
return Object.assign({}, oldVal, {
value: e.target.value
})
})
setIsValid(valid)
}
const onBlur = () => {
if (isValid) {
if (edited) updateMyData(index, id, value.value)
} else {
setValue(initialValue)
value.value != null && openSnackbar({ message: errorMsg, apiResponse: 'error' })
}
setEdited(false)
}
useEffect(() => {
setValue(initialValue)
}, [initialValue])
return <InputBase disabled={!value.editable} value={value.value != null ? value.value : ''} onChange={onChange} onBlur={onBlur} />
}
EditableCell.contextTypes = {
literal: PropTypes.func
}
export default EditableCell
My data model is as follows:
const data =[{
"id": 1,
"absltBendingStep": {
"value": 2,
"editable": false,
"colorCode": -1,
"type": "number"
},
"rltvBendingStep": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"circInterpolation": {
"value": null,
"editable": true,
"colorCode": -1,
"type": "bool"
},
"shape": {
"value": null,
"editable": true,
"colorCode": -1,
"type": "bool"
},
"xClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"tip": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"headUpperClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"headLowerClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"duPlate": {
"value": 15.75706,
"editable": true,
"colorCode": -1,
"type": "number"
},
"xConf": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"yConf": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"angle": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"description": {
"value": "15.8",
"editable": false,
"colorCode": -1,
"type": "string"
},
"upperClamp": {
"value": 0,
"editable": false,
"colorCode": -1,
"type": "number"
},
"time": {
"value": 0,
"editable": false,
"colorCode": -1,
"type": "number"
},
"observations": {
"value": "",
"editable": false,
"colorCode": -1,
"type": "string"
}
},
{
"id": 2,
"absltBendingStep": {
"value": 3,
"editable": false,
"colorCode": -1,
"type": "number"
},
"rltvBendingStep": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"circInterpolation": {
"value": null,
"editable": true,
"colorCode": -1,
"type": "bool"
},
"shape": {
"value": null,
"editable": true,
"colorCode": -1,
"type": "bool"
},
"xClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"tip": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"headUpperClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"headLowerClamp": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "string"
},
"duPlate": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"xConf": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"yConf": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"angle": {
"value": null,
"editable": false,
"colorCode": -1,
"type": "number"
},
"description": {
"value": "",
"editable": false,
"colorCode": -1,
"type": "string"
},
"upperClamp": {
"value": 0,
"editable": false,
"colorCode": -1,
"type": "number"
},
"time": {
"value": 0,
"editable": false,
"colorCode": -1,
"type": "number"
},
"observations": {
"value": "",
"editable": false,
"colorCode": -1,
"type": "string"
}
}]