【问题标题】:Rending stateful components from array - grandchild state and parent state not aligning从数组中渲染有状态组件 - 孙状态和父状态未对齐
【发布时间】:2020-03-12 16:40:50
【问题描述】:

我正在编写一个用户可编辑的组件,该组件将用户更改保持在其状态内。我希望能够以 2 种方式使用该组件:1: 一次由作者硬编码,或 2: 从父组件状态的数组生成.在第二种情况下,我无法同步状态。我希望组件是可移除的,所以它有一个“移除我”按钮,它应该通过使用回调函数 prop 与父级的状态进行通信。

场景:

假设我有一个父组件,其状态中有一个数组。从此数组中,子组件使用.map 语句呈现:

// in ParentComponent.js:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 2"},
    {coords: Array(2), popupContent: "Popup 3"},
    ... etc ...
  ]
}

// In the return:

this.state.markers.map( (marker, index) => (
  <Marker key={index}>
    <Popup sourceKey={index} 
      setContentCallback={this.saveContentToState} 
      removalCallback={this.removalCallback} >
      {marker.popupContent}
    </Popup>
  </Marker>
))

有问题的组件是孙子&lt;Popup/&gt;。在 Popup.js 中,用户可以对组件的内容进行更改,这些更改会保存在 Popup 的状态中:

// In Popup.js

   state = { 
     content: this.props.children,
     inputValue: this.props.children
   }

   onEditHandler = { 
     this.setState({inputValue: e.target.value}) 
   }

   saveEdits = () => {
      if (this.props.saveContentCallback){
         this.props.saveContentCallback(this.state.inputValue, this.props.sourceKey)
      }
      this.setState({
         content: this.state.inputValue,
      })
   }

   removeSource = () => {
      if(this.props.removalCallback){
         this.props.removalCallback(this.props.sourceKey)
      } else {
         // internal leaflet function to remove a popup from a map
         this.thePopup.leafletElement._source.remove()
      }
   }

// Within the return function:
   return ( 
     <>
       <ContentEditableDiv onChange={this.onEditHandler}>
         { this.state.content }
       </ContentEditableDiv>
       <div onClick={this.saveEdits}>Save</div>
       <div onClick={this.removeSource}>Remove me</div>
     </>
   )

您可以通过saveEdits 函数查看组件如何在上述任何一种情况下保持其自身状态的变化。但是为了与父母的状态交流变化,它使用了道具removalCallbacksaveContentCallback。所以,回到ParentComponent.js

  removalCallback = index => {
    mapRef.current.leafletElement.closePopup()
    this.setState(prevState => {
       prevState.markers.splice(index, 1)
       return {
          markers: prevState.markers
       }
    })
  }

   saveContentToState = (content, index) => {
      this.setState( prevState => {
         const newMarkers = prevState.markers
         newMarkers[index].popupContent = content
         return {
            ...this.state.newMarkers
         }
      })
   }

预期行为

当点击弹出窗口上的“删除”按钮时,我希望回调被调用。当回调被调用时,它应该从 ParentComponent 的状态数组 'markers' 中删除该弹出窗口,并且 ParentComponent 应该只使用剩余的标记重新渲染,以及它们关联的popupContent。例如,如果我从这个数组开始:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 2"},
    {coords: Array(2), popupContent: "Popup 3"},
  ]
}

然后单击弹出窗口 2 上的 remove me 按钮,我应该得到这个数组:

state = {
  markers: [
    {coords: Array(2), popupContent: "Popup 1"},
    {coords: Array(2), popupContent: "Popup 3"},
  ]
}

带有两个带有弹出窗口的标记,上面写着“弹出窗口 1”和“弹出窗口 3”。

实际行为 - 错误

所以我确实在 ParentComponent 状态中得到了期望数组,就像我在上面写的那样。但是,弹出窗口的内部状态不合作。当我单击弹出窗口 #2 上的 remove me 按钮时,我得到 2 个弹出窗口,但它们的内容是“弹出窗口 1”和“弹出窗口 2”。当我查看每个 &lt;Popup /&gt; 组件的内部状态时,每个组件的 content 分别是“Popup 1”和“Popup 2”。就好像当ith 弹出窗口被删除时,它的内部状态以某种方式转移到i+1th 弹出窗口,该弹出窗口通过数组中的所有弹出窗口进行传输。

Working Demo of the Problem

这是一个 react-leaflet 项目,但我觉得这是一个 react 状态管理问题。打开代码框的渲染,你会看到 5 个弹出窗口。当您在任何弹出窗口(最后一个除外)上单击“删除我”时,您会看到所有弹出窗口的数字都发生了变化。在 react 开发工具组件选项卡中,您将看到 &lt;Map /&gt;(即&lt;ParentComponent&gt;)状态数组正在正确更新。但是查看每个 &lt;EditablePopup /&gt; 内部状态,这些与父 (&lt;Map>) 组件的状态不对应。我知道像state = { content: this.props.something } 这样的东西会导致问题,但我不确定这是否是这种情况的罪魁祸首。

这里出了什么问题?每次removalCallbacksaveContentCallback 触发时,这些&lt;Marker/&gt;&lt;Popup/&gt; 组件不应该所有重新渲染,因为它会更新父级的状态并应该触发父级的重新渲染? 我尝试在父组件内的这些回调中添加this.forceUpdate,但没有任何效果。抱歉问了这么长的问题,感谢阅读

【问题讨论】:

    标签: reactjs callback setstate react-leaflet react-state-management


    【解决方案1】:

    原因是您没有为列表中的每个子元素传递唯一键。如果没有唯一键,React 无法区分元素是被删除还是只是内容被改变。

    所以,当你删除项目时,React 的 diff 算法认为只有内容发生了变化,因为密钥没有改变

    最简单的测试方法是更改​​popupContentlike 的密钥。

    <Marker key={marker.popupContent} position={marker.coords}>
      ...
    </Marker>
    

    但这不是解决方案。为每个元素创建一个唯一键,以防止将来出现问题。

    更多关于 React 的 diff 算法如何工作的信息

    How Diff Algorithm is implemented in Reactjs?

    确保您的密钥是唯一的!

    【讨论】:

    • 是的!就是这样。我读过一两次,在.map 语句中使用index 作为key 是一个坏主意,但我从来不明白为什么。这种情况正是原因。因为密钥不是唯一的,所以奇怪的事情开始发生。我使用uuid 包将key={index} 替换为key={uuidv4()}。只要允许,我就会奖励赏金。我觉得解决方案很简单,感谢您花时间阅读我的长问题。
    • @SethLutske 这总是一种乐趣
    【解决方案2】:

    您需要更改editablePopup.jsline136 以获取最新的孩子。

    {Parser(this.props.children)}

    工作示例

    https://codesandbox.io/s/removal-callback-question-z9q6i

    我还注意到您正在改变旧状态而不是新状态。

           const newMarkers = prevState.markers // But here you're mutating the old state. you should mutate copy of the state.
           newMarkers[index].popupContent = content
           return {
              ...this.state.newMarkers  
           }
    
    

    下面同样拼接你正在改变 prevState。

        this.setState(prevState => {
           prevState.markers.splice(index, 1) // User array filter with index !== i. So you will get a copy with filtered array. 
           return {
              markers: prevState.markers
           }
        })
    
    

    【讨论】:

    • 所以这确实有效,但仅适用于我的问题开头提到的场景 2。它之所以有效,是因为每次保存某些内容时,它都会与父状态通信,如果它发生变化,则 @​​987654328@ 已更改,并重新渲染。但是,这在场景 1 中不起作用,其中组件是硬编码的,并且内容不是从父状态读取,而是从其自身状态读取。查看this codesandbox。此外,您的解决方案绕过了我的 parsedChildren 子句
    • 我曾想过有条件地渲染内容,比如removalCallBacksaveContentCallBack 是否存在,我可以为您提供解决方案。但是上面塞尔吉奥的解决方案更全面一些。
    猜你喜欢
    • 2017-02-03
    • 2021-04-06
    • 2019-11-09
    • 2019-04-29
    • 2021-03-07
    • 2018-12-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多