删除行时更新单元格背景颜色。 (反应-table)

Update cell background color when deleting the row. (react-table)

我正在使用 react-table 库在其 7.6.2 版 中渲染 table .

table(是分页的 table)具有添加或删除行以及编辑单元格值的功能。 每次用户添加新行或编辑单元格时,单元格或行都会更新为蓝色背景。

到目前为止,一切正常。删除行时会出现问题。 从数据结构中删除该行后,该行从table中移除,但是该行的颜色保留在table中,直到更新分页.

我的生产应用程序使用 redux,但我创建了一个简化的 sandbox 来重现错误。

我已验证 table数据 已正确更新。

    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

我的自定义单元格组件:

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

我的数据模型如下:


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"
        }
    }]

这个问题的主要原因是按键。要了解有关如何使用密钥的更多信息,您可以在此处查看 React 的官方文档:https://reactjs.org/docs/lists-and-keys.html#keys

造成这种不一致的主要原因是这里的代码

<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>

ClickAndHold 组件从 row.getRowProps() 传递了 props。 row.getRowProps() returns 包含看起来像 row_0 的键的对象。现在,此键取决于行在 table 中的位置。假设有五行,那么它们的键就是row_0row_1row_2row_3row_4。如果删除第 4 行(键 row_3),第五行(键 row_4)将获得第四行的键。假设您实际上删除了第四行,那么键将如下所示:row_0row_1row_2row_3。所以,现在,第五行(以前有键 row_4,但现在有键 row_3)具有第四行的键。因此,当 react 重新渲染你的树时,它会将第四行的道具传递给第五行。这意味着如果第四行有蓝色背景,那么第五行也会有蓝色背景。我知道这是少数,但我希望我在这里说得通。

要解决此问题,您需要将唯一键传递给该行。理想情况下,这个唯一键应该来自您正在呈现的数据。如果我查看您的数据,您的 id 是独一无二的。因此,使用此 id 作为 ClickAndHold 组件的键。总结一下,要解决这个问题,您需要将代码编辑为

<tbody {...getTableBodyProps()}>
  {page.map((row, i) => {
    prepareRow(row)
    return (
      <ClickAndHold id={i} elmType={'tr'} onHold={e => openContextMenu(e, row)} {...row.getRowProps()} key={row.original.id} onContextMenu={e => openContextMenu(e, row)}>
        {row.cells.map(cell => {
          return <td {...cell.getCellProps([getCellProps(cell)])}>{cell.render('Cell')}</td>
        })}
      </ClickAndHold>
    )
  })}

page 列表中的 row 对象包含一个包含您的数据的 original 对象。因此,您只需使用自定义数据中的 id,并将其用作 key。您需要在 {...row.getRowProps()} 之后传递 key 以覆盖 row.getRowProps().

返回的密钥

我已经在你的 codesandbox 中测试过了,你只需要用这种方式编辑 CustomTable.jsx 中第 85 行中找到的 tr 组件。

<tr
  id={i}
  {...row.getRowProps()}
  key={row.original.id}
  onContextMenu={(e) => openContextMenu(e, row)}
>

希望对您有所帮助。

另一个建议,在您添加新行的代码中,您正在更改新添加行之后的所有 ID。这是参考代码。

setTableData((sequence) => {
    const newData = sequence.reduce((acc, step) => {
      if (incrementId) {
        step.id = step.id + 1;
        acc.push(step);
      } else {
        acc.push(step);
      }
      if (step.id === nextToId) {
        newStep.id = newStep.id + 1;
        acc.push(newStep);
        incrementId = true;
      }

      return acc;
    }, []);
    return newData;
  });

这将导致不一致,因为当您更改用作行的键的 id 时,在下一次重新渲染时,React 会将 props 传递给属于新添加的前一行的新添加的行添加行替换。要了解更多信息,请查看这篇文章:https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318. What I'm trying to say is, keys should be unique for every component inside the list, and if a component is added or deleted, any other component cannot take its key. You can use uuid for generating unique keys. Take a look here for how to use uuid https://www.npmjs.com/package/uuid.

本质上,您需要小心处理键,否则您可能会严重降低应用程序的性能,或搞乱组件树的状态。

更新
抱歉,我错了这个问题的根本原因。虽然您使用按键的方式确实存在问题,但背景颜色问题并不仅仅是因为它。其实导致背景颜色不变的原因是你设置了background-color: nonebackground-color 没有名为 none 的 属性。所以,这是一个无效的 CSS 属性,它不会在 DOM 中更新。这会导致以前有效的背景颜色徘徊,从而导致问题。要解决此问题,您可能需要在要删除蓝色背景时设置 background-color: unsetbackground-color: white。希望对您有所帮助!