警告:setState(…):无法在现有状态转换期间进行更新

4

我正在开发一个简单的“待办事项清单”React应用程序(对React.js不太熟悉)。我已经实现了向列表中添加项目,但删除项目则引起了一个问题。在我的父级React组件中,我有以下代码:

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函数中,如 <li onClick={removeCallback.bind(null, id)} />,结果不起作用?


所以如果我理解正确,您正在询问为什么 onClick={removeCallback(id)} 会立即触发/调用 removeCallback,而 onClick={removeCallback.bind(null, id)} 却不会立即触发/调用 removeCallback? - John
没错。JavaScript中的this上下文和函数回调,即functionName()function,让我感到困惑已经有一段时间了。@John - soups
3个回答

4
问题在这部分:
onClick={removeCallback(id)}

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

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

这就是你遇到错误的原因。
检查一下代码片段中abcabc()之间的区别。

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文档:
bind()函数创建一个新的绑定函数(BF)。BF是一个外来的函数对象(来自ECMAScript 2015的术语),它包装了原始函数对象。调用BF通常会导致其包装函数的执行。
请查看React文档:JSX中的事件处理
欲了解有关绑定的更多详细信息,请参阅此答案:使用JavaScript“bind”方法

谢谢@mayank。如果'removeCallback()'调用函数并分配结果,我如何从ToDoEntries将参数传回父ToDoList组件? - soups
2
@soups 我不是React专家,但我看到两个解决方案,你可以像现在这样使用bind,或者你可以创建一个包装函数,像这样 onClick={()=>removeCallback(id)}(请注意,代码使用了箭头函数)。 - John
1
Soups,抱歉但我想让你知道@mayank没有在评论中回答你的问题,是我回答了。Mayank似乎是一个React专家。;) - John
2
@John 我不是 React 专家,回答完这个问题后我就睡了,所以没有回复你的评论,谢谢 :) - Mayank Shukla
@john 对不起,我之前在评论中错误地标记了Mayank。:] - soups
1
@soups 不,使用箭头函数而不是 .bind 做的事情完全相同。你应该避免这种解决方案,因为它会在每次渲染时创建一个新函数。 - Win

2
我建议不要使用当前的方法,而是采用类似我为您编写的示例的方法。渲染一个待办事项列表绑定到状态,然后将相关信息传递给父组件以删除该项目。在这种情况下,我使用待办事项的索引来拼接数组以将其删除。
您当前的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'));
    <div id="app"></div>


  • 我一直在寻找避免使用内联.bind()的方法,我看到了一篇解释为什么这是一个不好的想法的文章,但我不知道如何在我的示例中实现它。谢谢你。请耐心等待,但我可以假设您的解决方案有效,因为const ToDoEntries没有render()吗?@Win - soups
    1
    @soups 正确。只有在单击元素时,该函数才会执行,因为它是类构造函数的一部分,所以不会每次渲染时创建新函数,这被认为是繁重的。 - Win
    我已经为你的解决方案苦恼了很久。App#removeItem() 如何接收参数,因为当 Todo 调用该块时,它没有任何参数。换句话说,App#removeItem() 如何知道在 state: list[] 中不包含哪个项目?@Win - soups
    1
    @soups App#removeItem 从子组件接收有效载荷,这里是 Todo。当我在 ToDoEntries 中呈现待办事项时,我也传递了索引。因此,当用户单击元素时,它还会将有效载荷中的属性中的索引传递回父组件,以便它现在可以从状态中删除待办事项(使用 slice),因为我们知道数组中待办事项的索引。 - Win
    我错过了Todo#remove执行回调并传递参数的部分。谢谢@Win。 - soups

    2

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

    嗯,当您映射待办事项列表时,每个<li/>都会调用removeCallback函数,而不是将其分配给onClick

    因此,当前的代码:

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

    相当于:

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

    您正确指出使用bind会起作用,这是由于其行为在这些情况下非常有用。请参见mdn文档获取更多信息,但我会在此引用重要部分:

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

    在您的情况下,当使用绑定时,并将其提供给onClick时,您正在创建一个新函数,该函数将在实际触发单击事件时调用,而不是在元素被呈现时调用。
    另一种看待removeCallback.bind(null, id)的方式是像这样的:
    var newFunc = () => {
      return removeCallback(id);
    }
    <li onClick={newFunc} </li>
    

    我现在开始理解bind()了,之前我一直在苦恼这个概念以及this上下文。总的来说,bind()创建一个新函数,但作为一个回调函数不会立即执行。这是JavaScript、React还是ES6的特性呢? - soups

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接