【问题标题】:After updating props, this.props is always === nextProps / prevProps更新 props 后 this.props 总是 === nextProps / prevProps
【发布时间】:2014-09-19 07:01:12
【问题描述】:

我在这里有一个解决这个问题的 jsbin:http://jsbin.com/tekuluve/1/edit

在 onClick 事件中,我从模型中删除一个元素,并重新渲染应用程序。但奇怪的是,在 componentWillReceiveProps()(以及 componentWillUpdate 和 componentDidUpdate)中,nextProps 总是 === 到 this.props,不管我做什么。

/** @jsx React.DOM */
var Box = React.createClass({
  render: function() {
    return (
      <div className="box" onClick={ UpdateModel }>
        { this.props.label }
      </div>
    );
  }
});

var Grid = React.createClass({
  componentWillReceiveProps: function(nextProps) {      
    // WTF is going on here???
    console.log(nextProps.boxes === this.props.boxes)        
  },
  render: function() {
    var boxes = _.map(this.props.boxes, function(d) {
      return (<Box label={ d.number } />);
    });

    return (
      <div className="grid">
        { boxes }
      </div>
    );
  }
});

var model = [
  { number: 1 },
  { number: 2 },
  { number: 3 },
  { number: 4 },
  { number: 5 }
];

function UpdateModel() {
  React.renderComponent(
    <Grid boxes={ _.pull(model, _.sample(model)) } />,
    document.body
  );
}

React.renderComponent(
  <Grid boxes={ model } />,
  document.body
);

在 componentWillReceiveProps() 生命周期事件中通过 UpdateModel() 更新后,我需要 nextProps 与 this.props 不同。

【问题讨论】:

  • 你为什么不使用状态?
  • 因为这不适合我正在构建的用例。我正在尝试构建一些生命周期方法,这些方法允许我将动态道具传递给卸载组件,所以首先我需要确定哪些组件正在卸载,因此在 componentWillReceiveProps() 中进行比较。
  • 我遇到了同样的问题。当调用componentWillReceiveProps时,当我尝试访问this.props时,它已经与nextProps相同。我无法访问旧道具来比较它们。我的 props 肯定发生了变化,在组件收到 props 之前,我无法访问 props 是什么。

标签: javascript reactjs


【解决方案1】:

类似的事情发生在我使用 Flux Stores 以官方 Todo List 教程 (http://facebook.github.io/flux/docs/todo-list.html) 建议的方式存储我的状态。对于按照教程后发现此问题的任何其他人,似乎在 getAll() 方法中的 TodoStore 中出现了问题,因为它返回对内部数据对象的直接引用:

getAll: function() {
  return _todos;
}

这似乎破坏了诸如 componentDidUpdate(prevProps) 之类的生命周期方法区分新旧道具的能力。我认为原因是通过将 Store 的数据对象的直接引用传递到视图中,当 Store 更改时状态/道具有效地立即更改,而不是在通过生命周期方法传递新值之后,所以新旧道具总是包含相同的值。这可以通过传递内部数据对象 _todos 的副本而不是对象本身来解决,

例如,当 _todos 是一个对象时,

getAll: function() {
  return JSON.parse(JSON.stringify(_todos));
}

如果 _todos 是一个数组,可以使用 return _todos.slice() 代替。

一般来说,如果使用 Store 来包含状态信息并从控制器视图中调用它,似乎建议返回数据的副本而不是对原始数据的引用,因为当状态更改时原始数据会发生变化发生。

【讨论】:

  • 我们是否需要为每个状态变化创建新对象?
  • 如果您想避免每次都复制整个状态,我认为您可以使用更具体的更改事件和 getter 函数来获取更改的部分并更新控制器视图的现有状态区别。
  • 请注意,这种方法和各种 cloneDeep 方法(例如使用 lodash/underscore)将无法用于某些本地对象,例如您从 &lt;input type='file'&gt; 获得的 Files,因为您会丢失使用的本地方法在上传过程中(并不是每个属性都是可序列化的)
  • 这为我解决了。我已经实现了 Flux 并且确实作为道具传递了对商店中对象的直接引用。我最终使用 lodash' _.assign({}, object) 来复制对象。
  • 我们在大规模应用程序中使用了类似的方法,但它导致了严重的性能问题。我们最初的方法在一个重要方面有所不同——我们使用字符串比较来模拟 PureRenderMixin 以支持深度检查。在 SCU 中,我们会检查 JSON.parse(JSON.stringify(this.props) === JSON.parse(JSON.stringify(nextProps)。这是一个糟糕的方法。对于这样一个经常调用的方法,操作太繁重了。现在我们只需确保在商店的更改上返回一个新的 obj,例如 this.state.someData = _.extend({}, newData)
【解决方案2】:

=== 将检查它是否是同一个对象。看来您正在做的是改变 boxes 属性指向的值。

【讨论】:

  • 将此标记为答案,因为它有点接近。我通过添加 model = model.slice(); 解决了这个问题到 UpdateModel() 方法的第一行。
  • 如果你想在不调用 setState 的情况下改变模型,你可以随时在 React.renderComponent 返回的组件实例上调用 forceRender() :)
【解决方案3】:

您可以使用 Immutable.js。这是 Facebook 为使用 react/flux 而设计的一个库。 https://facebook.github.io/immutable-js/

我们已经被这件事难住了好几次了。

shouldComponentUpdate(nextProps) { 
    return this.props.myObj !== nextProps.myObj
}

很多时候,myObj 对象中的某些值会发生变化。然而,上面的函数会返回 false。原因是this.props.myObjnextProps.myObj 都在引用/指向同一个对象。

通过实现 Immutable.js,数据将始终作为克隆向下传递(而不是引用同一个对象,它们实际上是单独的对象)。

它增强了通量的单向流动。您将永远无法(意外)修改传递到组件中的原始数据(无论是作为道具还是来自通量存储)。

这也使您能够使用 PureRenderMixin - 如果 state/props 的值没有改变,它会自动停止渲染。这有助于提高性能。

【讨论】:

    【解决方案4】:

    最后我修好了。 问题是在没有深度克隆的情况下更改道具。

    我的道具是array of objects,我在父级上更改它:

    let newtodos = [...this.state.todos];
    newtodos[0].text = 'changed text';
    this.setState({ todos : newtodos });
    

    所以在componentDidUpdate 我得到了错误的结果:

    componentDidUpdate(prevProps){
      //prevProps.todos is always equal to this.props.todos, since you changed the prop object directly.
      if(JSON.stringify(prevProps.todos) !== JSON.stringify(this.props.todos){...}
    }
    

    所以我修复了它:

    // let newtodos = [...this.state.todos]; // SHALLOW COPY - Wrong
    let newtodos = JSON.parse(JSON.stringify(this.state.todos)); // Deep Clone - Right
    newtodos[0].text = 'changed text';
    this.setState({ todos : newtodos });
    

    【讨论】:

      【解决方案5】:

      实现 Immutable.js 的一种替代方法是使用对象扩展运算符。

      举个例子,

      假设你有一个想要修改的变量,然后做一些事情

      const obj1 = {a: 1, b: 2}
      const obj2 = testObj
      obj2.a: 3
      
      
      // result: obj1 = obj2 = {a:3, b:2}
      

      原始对象将被修改。这是因为 obj2 变量本质上只是一个指向 obj1 对象的指针。

      现在假设我们改用扩展运算符

      const obj1 = {a: 1, b: 2}
      const obj2 = {...testObj}
      obj2.a: 3
      
      // result: obj1 = {a:1, b:2}, obj2 = {a:3, b:2}
      

      这样做的结果是 obj2 正确更新,但 obj1 保持不变。扩展运算符有效地创建了 obj1 的浅层克隆。我发现扩展运算符在修改 redux reducer 中的状态时特别有用。如果你直接将变量设置为 state 并对其进行修改,就会导致你遇到的问题。

      【讨论】:

        【解决方案6】:

        如果您不想使用传播,可以改为这样做。只需将您的 UpdateModel 函数更改为

        UpdateModel(){
          React.renderComponent(
            <Grid boxes={ _.clone(_.pull(model, _.sample(model))) } />, document.body
          );
        }
        

        否则,nextProp.boxes 对象与 this.props.boxes 对象是同一个对象,因此您对其所做的任何更改都会反映在两者中。

        【讨论】:

          猜你喜欢
          • 2019-10-27
          • 2016-08-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-10-20
          • 1970-01-01
          • 2016-08-13
          • 2021-07-01
          相关资源
          最近更新 更多