【问题标题】:Warning: setState(…): Cannot update during an existing state transition警告:setState(...):在现有状态转换期间无法更新
【发布时间】:2017-12-20 18:50:13
【问题描述】:

我正在开发一个简单的“待办事项”反应应用程序(React.js 的新功能)。我已将项目添加到列表中,但删除项目会引发问题。在我的父反应组件中,我有以下代码:

import ToDoEntries from './to_do_entries.jsx';

class ToDoList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { list: [] }
    this.add = this.addItem.bind(this);
    this.removeItem = this.removeItem.bind(this);
  }

  addItem(e) { //removed to avoid tl:dr }

  render() {
    return(
      <form onSubmit={this.add}>
        <input placeholder='Enter item' type='text' ref={(el) => {this._input = el;} }/>
        <button>Add</button>
      </form>

      <ToDoEntries entries={this.state.list}
        removeCallback={this.removeItem}
      />
    );
  }

}

我的to_do_entries.jsx 组件:

class ToDoEntries extends React.Component {
  constructor(props) {
    super(props);
  }

  renderItems() {
    const { entries, removeCallback } = this.props;

    function createTasks(item) {
      return <li key={item.key}>{item.text}</li>
    }

    var listItems = entries.map(function(item) {
      return(<li onClick={removeCallback} key={item.key}>{item.text}</li>)
    })

    return listItems;
  }

  render() {
    var todoEntries = this.renderItems();

    return(
      <ul>
        {todoEntries}
      </ul>
    );
  }
}

export default ToDoEntries;

运行这段代码带来:

警告:setState(…):在现有状态期间无法更新 过渡

问题:

为什么to_do_entries.jsx的渲染在添加项目时立即执行回调,即:

var listItems = entries.map(function(item) {
  return(<li onClick={removeCallback(id)} key={item.key}>{item.text}</li>)
})

但是,将.bind(null, id) 添加到 removeCallback 即。 &lt;li onClick={removeCallback.bind(null, id)} /&gt; 没有?

【问题讨论】:

  • 所以,如果我理解正确,您是在问为什么 onClick={removeCallback(id)} 会立即触发/调用 removeCallback 以及为什么 onClick={removeCallback.bind(null, id)} 不会立即触发/调用 removeCallback?
  • 完全正确。 Javascript 的 this 上下文和函数回调,即 functionName() 与 function 已经让我难以置信一段时间了。 @约翰

标签: javascript reactjs


【解决方案1】:

问题出在这部分:

onClick={removeCallback(id)}

我们需要向 onClick 传递一个函数,而不是值。当我们将() 与functionName 一起使用时,这意味着您正在调用该方法并将其结果分配给onClick,如果您在removeCallback 中执行setState,这将创建一个无限循环,因为这个周期:

render ->  removeCallback()  ->  setState ->
  ^                                         |
  |                                         |
  |                                         |
   -----------------------------------------

这就是您收到错误的原因。

检查 sn-p 以了解abc and abc() 之间的差异:

function abc(){
   return 'Hello';
}

console.log('without () = ', abc);     //will return the function
 
console.log('with () = ', abc());      //will return the function result (value)

为什么要使用onClick={removeCallback.bind(null, id)}?

因为 bind 会创建一个新函数,并将该函数分配给点击事件,所以这里removeCallback 将在您点击任何项目时被调用,而不是自动调用。

根据MDN Doc:

bind() 函数创建一个新的绑定函数 (BF)。 BF 是一个 外来函数对象(ECMAScript 2015 中的一个术语)包装了 原始函数对象。调用 BF 通常会导致 执行其包装函数。

检查React DOC: Handling events in JSX。

查看此答案以了解有关绑定的更多详细信息:Use of the JavaScript 'bind' method

【讨论】:

  • 谢谢@mayank。如果 'removeCallback()' 调用函数并分配结果,我如何将参数从 ToDoEntries 传递回父 ToDoList 组件?
  • @soups 我不是反应专家,但我看到的两个解决方案是您可以像现在一样使用绑定,或者您可以创建一个像 onClick={()=&gt;removeCallback(id)} 这样的包装函数(请注意,代码使用arrow function)
  • 汤,抱歉,只是想让你知道@mayank 没有在我所做的 cmets 中回答你的问题。 Mayank 似乎是一个反应专家。 ;)
  • @John 我不是反应专家,我在回答完这个问题后就睡着了,所以没有回答,谢谢你的评论:)
  • @soups 不,做一个箭头函数而不是 .bind 正在做同样的事情。作为解决方案,您需要避免这种情况,因为它会在每次渲染时创建一个新函数。
【解决方案2】:

我建议不要这样做,并使用与我为您编写的示例类似的方法。渲染绑定到状态的待办事项列表,然后将相关信息传递回您的父组件以删除该项目。在这种情况下,我使用 todo 的索引来拼接数组,以便将其删除。

当每个 todo &lt;li&gt; 被渲染时,您当前的 onClick 会立即被调用,因为它只是一个导致问题的函数调用。 .bind 解决了这个问题,因为它会在您单击元素时创建一个新函数,这就是该函数不会立即调用的原因。

但是,这通常被认为是不好的做法,因为每次组件都会一次又一次地创建此函数。将其乘以屏幕上待办事项的数量,您将失去性能。这是一个小问题,但我的示例显示了如何解决这个问题。 https://codepen.io/w7sang/pen/VWNLJp

// App
class App extends React.Component{
  constructor(props) {
    super(props);
    this.state = { list: [] }
    this.add = this.addItem.bind(this);
    this.removeItem = this.removeItem.bind(this);
  }
  addItem(e) { 
    e.preventDefault();
    this.setState({
      list: [ 
        ...this.state.list, 
        {
          key: Math.random(1,10000),
          text: this._input.value
        }
      ]
    })
  }
  removeItem(payload){
    this.setState({
      list: [ 
        ...this.state.list.slice(0, payload.index),
        ...this.state.list.slice(payload.index + 1)
      ]
    })
  }
  render() {
    return(
      <div>
        <form onSubmit={this.add}>
          <input placeholder='Enter item' type='text' ref={(el) => {this._input = el;} }/>
          <button>Add</button>
        </form>
        <ToDoEntries entries={this.state.list} removeItem={this.removeItem} />
      </div>
    );
  }
}

// TodoEntries [STATELESS]
const ToDoEntries = ({ entries, removeItem } ) => {
  return(
    <ul>
      { entries.map((item, index) => {
        return(<Todo key={item.key} index={index} item={item} removeItem={removeItem} />)
      }) }
    </ul>
  );
}

// Todo
class Todo extends React.Component {
  constructor(props){
    super(props);
    this.state = {};
    this.remove = this.remove.bind(this);
  }
  remove() {
    const { index, removeItem } = this.props;
    removeItem({
      index
    });
  }
  render() {
    return <li onClick={this.remove}>{this.props.item.text}</li>
  }
}

ReactDOM.render(<App />,document.getElementById('app'));
&lt;div id="app"&gt;&lt;/div&gt;

【讨论】:

  • 我正在寻找一种使用内联.bind() 的方法,我确实来了一篇文章,解释了为什么这是一个坏主意,但我不知道如何在我的示例中实现它。谢谢你。请耐心等待,但是由于const ToDoEntries 没有render() 的原因,我可以假设您的解决方案有效吗? @Win
  • @soups 正确。该函数仅在单击元素时才会执行,因为它是类构造函数的一部分,所以您不会在每次渲染时都创建一个新函数。每次渲染时创建一个新函数被认为是繁重的。
  • 我一直在关注您的解决方案。 App#removeItem() 是如何接收参数的,因为当Todo 调用该块时,它会在没有任何参数的情况下这样做。换句话说,App#removeItem() 如何知道哪个项目不包含在 state: list[] 中? @Win
  • @soups App#removeItem 接收来自子组件的有效负载,在本例中为Todo。当我在ToDoEntries 中渲染待办事项时,我也传递了索引。因此,当用户单击元素时,它还会将有效负载中 prop 的索引传递回父组件,以便它现在可以从状态中删除待办事项(带切片),因为我们知道数组中待办事项的索引.
  • 我错过了Todo#remove 执行传入参数的回调的部分。谢谢@Win
【解决方案3】:

为什么 to_do_entries.jsx 的 render 会立即执行回调?

好吧,当您映射到待办事项列表时,每个 &lt;li/&gt; 都在调用 removeCallback 函数,而不是将其分配给 onClick。

所以当前代码

<li onClick={removeCallback(id)} </li>

相当于:

var result = removeCallback(id);
<li onClick={result} </li>

您已经正确指出使用bind 会起作用。这是由于它在这些情况下如此有用的行为。

See the mdn docs for more info,但我会在这里引用重要的部分:

bind ... 创建并返回一个新函数,调用时...

在您的情况下,当使用绑定并将其提供给 onClick 时,您正在创建一个新函数,该函数将在实际触发 click 事件时调用,而不是在呈现元素时调用。

查看removeCallback.bind(null, id) 的另一种方式是这样的:

var newFunc = () => {
  return removeCallback(id);
}
<li onClick={newFunc} </li>

【讨论】:

  • 我现在开始掌握 bind(),我一直在努力解决这个概念以及 this 上下文。因此,作为一般脚注,bind() 创建了一个新函数,但作为不会立即执行的回调。这是 Javascript 还是 react 还是 ES6?
猜你喜欢
  • 2017-05-16
  • 2016-09-20
  • 2016-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-02-22
  • 1970-01-01
  • 2017-09-04
相关资源
最近更新 更多