【问题标题】:Update cell background color when deleting the row. (react-table)删除行时更新单元格背景颜色。 (反应表)
【发布时间】:2021-08-05 09:13:47
【问题描述】:

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

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

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

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

我已验证 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"
        }
    }]

【问题讨论】:

    标签: reactjs react-table react-table-v7


    【解决方案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 行(使用键 row_3),则第五行(使用键 row_4)将获得第四行的键。假设您实际上删除了第四行,那么键将如下所示:row_0row_1row_2row_3。所以,现在,第五行(以前有键 row_4,但现在有键 row_3)有第四行的键。因此,当 react re 渲染你的树时,它会将第四行的道具传递给第五行。这意味着如果第四行有蓝色背景,那么第五行也将有蓝色背景。我知道这是少数,但我希望我在这里有意义。

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

    我已经在您的代码框中对此进行了测试,您只需以这种方式编辑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。我想说的是,列表中每个组件的键都应该是唯一的,如果添加或删除了一个组件,任何其他组件都不能获取它的键。您可以使用 uuid 生成唯一密钥。看这里如何使用uuidhttps://www.npmjs.com/package/uuid

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

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

    【讨论】:

    • 我已针对问题的实际根本原因编辑了答案。请看底部。
    • 你是对的。我将使用您提到的密钥管理改进来更新我的解决方案,即使错误不存在。
    猜你喜欢
    • 2019-11-23
    • 1970-01-01
    • 2018-12-20
    • 1970-01-01
    • 2013-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多