删除行时更新单元格背景颜色。(react表)

IT技术 reactjs react-table react-table-v7
2022-07-23 23:42:31

我正在使用7.6.2 版本的react-table库渲染一个表

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

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

我的生产应用程序与 redux 一起工作,但我创建了一个简化的沙箱来重现该错误。

我已验证tableData已正确更新。

    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"
        }
    }]
1个回答

这个问题的主要原因是键。要了解有关如何使用密钥的更多信息,您可以在此处查看 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(). row.getRowProps()返回一个对象,该对象包含一个看起来像row_0. 现在,这个键取决于行在表中的位置。假设有五行,那么它们的键将是row_0row_1row_2row_3row_4如果您删除了第 4 行(带有 key row_3),则第五行(带有 key row_4)将获得第四行的 key。假设您实际上删除了第四行,那么键将如下所示:row_0, row_1, row_2, row_3所以,现在,第五行(以前有 key row_4,但现在有 keyrow_3),有第四行的键。因此,当 react re 渲染你的树时,它会将第四行的props传递给第五行。这意味着如果第四行有蓝色背景,那么第五行也将有蓝色背景。我知道这是少数,但我希望我在这里有意义。

要解决此问题,您需要将唯一键传递给行。理想情况下,此唯一键应来自您正在呈现的数据。如果我查看您的数据,您将拥有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>
    )
  })}

列表中的对象包含一个包含row的数据的对象。因此,您只需使用自定义数据,并将其用作. 您需要通过after来覆盖.pageoriginalidkeykey{...row.getRowProps()}row.getRowProps()

我已经在您的代码框中对此进行了测试,您只需在其中以这种方式编辑第tr85 行中找到的组件。CustomTable.jsx

<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 。我想说的是,列表中每个组件的键都应该是唯一的,如果添加或删除了一个组件,任何其他组件都不能获取它的键。您可以使用 uuid 生成唯一密钥。在这里查看如何使用 uuid https://www.npmjs.com/package/uuid

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

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