在componentDidUpdate()内部的setState()


131

我正在编写一个脚本,该脚本根据下拉列表的高度和输入在屏幕上的位置将下拉列表移动到输入下方或上方。我也想将修改器设置为根据其方向下拉。但是setState在内部使用componentDidUpdate会产生无限循环(这很明显)

我已经找到了使用getDOMNode和直接将classname设置为下拉列表的解决方案,但是我觉得应该使用React工具有更好的解决方案。有谁能够帮我?

这是工作代码的一部分getDOMNode(略微忽略了定位逻辑,以简化代码)

let SearchDropdown = React.createClass({
    componentDidUpdate(params) {
        let el = this.getDOMNode();
        el.classList.remove('dropDown-top');
        if(needToMoveOnTop(el)) {
            el.top = newTopValue;
            el.right = newRightValue;
            el.classList.add('dropDown-top');
        }
    },
    render() {
        let dataFeed = this.props.dataFeed;
        return (
            <DropDown >
                {dataFeed.map((data, i) => {
                    return (<DropDownRow key={response.symbol} data={data}/>);
                })}
            </DropDown>
        );
    }
});

这是带有setstate的代码(它创建一个无限循环)

let SearchDropdown = React.createClass({
    getInitialState() {
        return {
            top: false
        };
    },
    componentDidUpdate(params) {
        let el = this.getDOMNode();
        if (this.state.top) {
           this.setState({top: false});
        }
        if(needToMoveOnTop(el)) {
            el.top = newTopValue;
            el.right = newRightValue;
            if (!this.state.top) {
              this.setState({top: true});
           }
        }
    },
    render() {
        let dataFeed = this.props.dataFeed;
        let class = cx({'dropDown-top' : this.state.top});
        return (
            <DropDown className={class} >
                {dataFeed.map((data, i) => {
                    return (<DropDownRow key={response.symbol} data={data}/>);
                })}
            </DropDown>
        );
    }
});

9
我觉得这里的技巧是,setState始终触发重新绘制。无需多次检查state.top和调用setState,只需跟踪要state.top在局部变量中显示的内容即可,然后仅在您的局部变量不匹配时在componentDidUpdate调用结束时跟踪一次。就目前情况而言,您在第一次重新渲染后立即重置,这将使您陷入无限循环。setStatestate.topstate.top
兰迪·莫里斯

2
见的两种不同的实现componentDidUpdate这个小提琴
兰迪·莫里斯

该死的!局部变量解决了整个问题,我怎么没想到mysef!谢谢!
卡捷琳娜·帕夫连科

1
我认为您应该接受以下答案。如果您再读一遍,我想您会发现它确实回答了最初的问题。
兰迪·莫里斯

为什么没有人建议将病情迁入componentShouldUpdate
帕特里克·罗伯茨

Answers:


116

你可以setState在里面使用componentDidUpdate。问题在于,由于没有中断条件,因此您正在以某种方式创建无限循环。

基于您需要呈现组件后浏览器提供的值的事实,我认为您的使用方法componentDidUpdate是正确的,它只需要更好地处理触发的条件即可setState


4
“破损条件”是什么意思?检查状态是否已经设置并且未重置?
卡捷琳娜·帕夫连科

我同意这一点,我唯一的补充意见是,添加/删除类可能在其中是不必要的,componentDidUpdate而可以根据需要在其中添加render
兰迪·莫里斯

但是类的添加/删除取决于在componentDidUpdate中签入的下拉位置,您建议对其进行两次检查?而且据我了解,componentDidUpdate被称为AFTER render(),因此在render()中添加/删除类是无用的
Katerina Pavlenko 2015年

我用setstate添加了我的代码,可以检查一下并指出我的错误吗?或向我展示一些不会引起循环的示例
Katerina Pavlenko 2015年

2
componentDidUpdate(prevProps,prevState){if(prevState.x!== this.state.x){//做某事}}
Ashok R

68

componentDidUpdate签名void::componentDidUpdate(previousProps, previousState)。有了它,您将能够测试哪些道具/状态是肮脏的,并进行相应的调用setState

例:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}

componentDidMount没有任何参数,仅在创建组件时调用,因此不能用于描述的目的。
Jules

@Jules谢谢!我曾经写过文章componentDidMount,所以当我写下答案时,著名的名字就层叠了😮再次,谢谢,谢谢!
Abdennour TOUMI

componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R

我知道您的关注@AshokR。您减少arg名称。但是“上一个”可能意味着不能上一个.. hhh。.kidding :)
Abdennour TOUMI

58

如果在setState内部使用componentDidUpdate它会更新组件,从而导致调用componentDidUpdate,随后setState再次调用该调用会导致无限循环。您应该有条件地致电setState并确保违反电话的条件最终会发生,例如:

componentDidUpdate: function() {
    if (condition) {
        this.setState({..})
    } else {
        //do something else
    }
}

如果您仅通过向其发送道具来更新组件(除了componentDidUpdate内部的情况,setState不会对其进行更新),则可以调用setState内部componentWillReceiveProps而不是componentDidUpdate


2
旧问题,但不建议使用componentWillReceiveProps,而应使用componentWillRecieveProps。您不能在此方法内设置状态。
Brooks DuBois

你是说getDerivedStateFromProps
adi518

5

这个例子将帮助您理解React Life Cycle Hooks

您可以setStategetDerivedStateFromPropsmethod中,static并在prop更改后触发该方法componentDidUpdate

在中,componentDidUpdate您将获得从返回的第三个参数getSnapshotBeforeUpdate

您可以检查此代码和框链接

// Child component
class Child extends React.Component {
  // First thing called when component loaded
  constructor(props) {
    console.log("constructor");
    super(props);
    this.state = {
      value: this.props.value,
      color: "green"
    };
  }

  // static method
  // dont have access of 'this'
  // return object will update the state
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    return {
      value: props.value,
      color: props.value % 2 === 0 ? "green" : "red"
    };
  }

  // skip render if return false
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    // return nextState.color !== this.state.color;
    return true;
  }

  // In between before real DOM updates (pre-commit)
  // has access of 'this'
  // return object will be captured in componentDidUpdate
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    return { oldValue: prevState.value };
  }

  // Calls after component updated
  // has access of previous state and props with snapshot
  // Can call methods here
  // setState inside this will cause infinite loop
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate: ", prevProps, prevState, snapshot);
  }

  static getDerivedStateFromError(error) {
    console.log("getDerivedStateFromError");
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.log("componentDidCatch: ", error, info);
  }

  // After component mount
  // Good place to start AJAX call and initial state
  componentDidMount() {
    console.log("componentDidMount");
    this.makeAjaxCall();
  }

  makeAjaxCall() {
    console.log("makeAjaxCall");
  }

  onClick() {
    console.log("state: ", this.state);
  }

  render() {
    return (
      <div style={{ border: "1px solid red", padding: "0px 10px 10px 10px" }}>
        <p style={{ color: this.state.color }}>Color: {this.state.color}</p>
        <button onClick={() => this.onClick()}>{this.props.value}</button>
      </div>
    );
  }
}

// Parent component
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: 1 };

    this.tick = () => {
      this.setState({
        date: new Date(),
        value: this.state.value + 1
      });
    };
  }

  componentDidMount() {
    setTimeout(this.tick, 2000);
  }

  render() {
    return (
      <div style={{ border: "1px solid blue", padding: "0px 10px 10px 10px" }}>
        <p>Parent</p>
        <Child value={this.state.value} />
      </div>
    );
  }
}

function App() {
  return (
    <React.Fragment>
      <Parent />
    </React.Fragment>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


2

我要说的是,您需要检查状态是否已经具有您要设置的相同值。如果相同,则没有必要再次为相同的值设置状态。

确保像这样设置您的状态:

let top = newValue /*true or false*/
if(top !== this.state.top){
    this.setState({top});
}

-1

我有一个类似的问题,我必须将toolTip居中。在componentDidUpdate中的React setState确实使我陷入了无限循环,我尝试了其工作的条件。但是我发现在ref回调中使用给我提供了更简单,干净的解决方案,如果将内联函数用于ref回调,则每次组件更新都将面临null问题。因此,请在ref回调中使用函数引用并在其中设置状态,这将启动重新渲染

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.