子组件正确删除父状态的对象条目,但错误的子组件在重新渲染时被卸载

Child component correctly deletes parent's state's object entry, but wrong child is unmounted upon re-render

我有一个 Order 组件,它在 this.state.newItems 中保留一个 Javascript 对象,并为它的每个子对象呈现 OrderItem 组件

OrderItem 还接收到对父级状态进行操作的回调

我发现 javascript 对象在删除时正确更新,但卸载了错误的节点

视频: https://streamable.com/bjesf

如您所见,卸载了错误的 OrderItem 组件

相关代码:

export default class Order extends Component {
  constructor (props) {
    super (props)

    this.state = {
        newItems: null
    }

    if (this.props.isNew)
        this.data = null
    else {
        this.data = { ... this.props }
    }
  }

  /* _commit = () => {
    console.log(this.data)
    if (this.data.trim().length > 0)
      socket.emit('article:client:insert', { name: this.data })
  } */

  addNewOrderItem = async () => {
    let _ = this.state.newItems ? { ... this.state.newItems } : {}

    _[Date.now().toString()] = {
        articleData: {
          isNew: true,
          unitaryPrice: 0.0
        }
    }

    await this.setState({
        newItems: _
    })

    console.log(this.state.newItems)
  }

  deleteNewOrderItem = async id => {
      let _ = { ...this.state.newItems }

      console.log("Deleting " + id)
      delete _[id]

      await this.setState({
        newItems: _
      })

      console.log(this.state.newItems)
  }

  updateNewOrderItem = async (id, value) => {
      let _ = { ...this.state.newItems }
      _[id] = value

      await this.setState({
        newItems: _
      })

      console.log(this.state.newItems)
  }

  renderNewItems () {
      if (!this.state.newItems) return null

      let _ = []
      for (let _id in this.state.newItems)
          _.push(
            <OrderItem 
                articleData={this.state.newItems[_id].articleData}
                id={_id}
                onUpdate={this.updateNewOrderItem}
                onDelete={this.deleteNewOrderItem}
            />
          )

      return _
  }

  render () {
    const { data } = this

    return (
       // ...
        {
            this.renderNewItems()
        }
       // ...
    )
  }
}


export default class OrderItem extends React.Component {
  state = {
    base64img: null
  }

  constructor (props) {
    super (props)
    this.setData(props)
  }

  componentWillUnmount () {
    console.log("Will unmount " + this.props.id)
  }

  setData (props) {
    this.data = { ...props }
    delete this.data.onDelete
    delete this.data.onUpdate
  }

  handleImageInsert = (event) => {
    let objectFile = event.target.files[0]
    if (!objectFile) return

    let reader = new FileReader()

    reader.onload = upload => 
      this.setState({
        base64img: upload.target.result
      })

    reader.readAsDataURL(objectFile)
    this.data.articleData.newImage = objectFile

    this.updateData()
  }

  handleUnitaryPriceChange = e => {
      let newPrice = parseFloat(e.target.value)
      if (newPrice != NaN) {
        this.data.articleData.unitaryPrice = newPrice
        this.updateData()
      }
  }

  handleBriefChange = e => {
    this.data.articleData.brief = e.target.value
    this.updateData()
  }

  handleTableChange = update => {
    this.data = { ... this.data, ... update }
    this.updateData()
  }

  updateData = () => {
    this.props.onUpdate(this.props.id, this.data)
  }

  confirmDelete = () => {
    this.props.onDelete(this.props.id)
  }

  render () {
    const { data: { articleData } } = this

    return (
      <div>
        <input
          ref={ref => this.fileUploadRef = ref}
          onChange={this.handleImageInsert}
          style={{ display: 'none' }}
          type="file"
          accept="image/*"
        />
        <div className="orderTableImageColumn">
          {
            this.state.base64img &&
            <img src={this.state.base64img} style={{width: '100%'}} />
          }
          <div className="orderTableImageColumnControls">
            <Button
              variant="raised"
              className="print-hide"
              color="primary"
              style={{ display: 'inline-block' }}
              onClick={() => this.fileUploadRef.click()}
            >
              <PhotoCamera />
            </Button>&nbsp;&nbsp;
            <Button
              variant="raised"
              className="print-hide"
              color="primary"
              style={{ display: 'inline-block', backgroundColor: 'red' }}
              onClick={this.confirmDelete}
            >
              <Delete />
            </Button>
          </div>
        </div>
        <div
          style={{
            whiteSpace: 'normal',
            wordWrap: 'break-word',
            width: '65%',
            padding: '2%',
            paddingLeft: '1%',
            verticalAlign: 'top',
            display: 'inline-block'
          }}>
            <div style={{display: 'inline-block', width: '50%'}}>
              {
                !(articleData.isPending || articleData.isNew) && <span className='tableLabelSmall'>SKU</span>
              }

              <br /><br />
              <textarea 
                name="brief"
                className='articleBriefTextarea'
                onChange={this.handleBriefChange}
                value={articleData.brief ? articleData.brief : 'Descrizione'}
                />
            </div>
            <div style={{
              display: 'inline-block', 
              width: '47%',
              marginLeft: '1%',
              padding: '1%',
              verticalAlign: 'top',
              backgroundColor: 'gold'
            }}>
              <span className='tableLabelSmall'>PREZZO UNITARIO €</span>&nbsp;&nbsp;
              <input 
                type="text" 
                style={{padding: '5px', marginBottom: '3px'}} 
                oninput="this.value = this.value.replace(/[^0-9.]/g, ''); this.value = this.value.replace(/(\..*)\./g, '');" 
                name="unitaryPrice"
                placeholder='0'
                onChange={this.handleUnitaryPriceChange}
              />
              <br />
              <textarea
                name="needs"
                className='articleBriefTextarea'
                value={articleData.needs ? articleData.needs : 'Materiali, accessori, necessità'}
              />
            </div>
        </div>
      </div>
    )
  }
}

您需要在 <OrderItem /> 上设置 key 道具,以便 React 知道哪些道具对应于哪个组件实例:

renderNewItems () {
      if (!this.state.newItems) return null

      let _ = []
      for (let _id in this.state.newItems)
          _.push(
            <OrderItem 
                key={_id}
                articleData={this.state.newItems[_id].articleData}
                id={_id}
                onUpdate={this.updateNewOrderItem}
                onDelete={this.deleteNewOrderItem}
            />
          )

      return _
  }

控制台中的错误实际上也在警告您。

顺便说一句,如果不允许子组件改变父组件的状态,那么对您的代码进行推理会容易得多。