162

I'm writing a script which moves dropdown below or above input depending on height of dropdown and position of the input on the screen. Also I want to set modifier to dropdown according to its direction. But using setState inside of the componentDidUpdate creates an infinite loop(which is obvious)

I've found a solution in using getDOMNode and setting classname to dropdown directly, but i feel that there should be a better solution using React tools. Can anybody help me?

Here is a part of working code with getDOMNode (i a little bit neglected positioning logic to simplify code)

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>
        );
    }
});

and here is code with setstate (which creates an infinite loop)

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>
        );
    }
});
Luke Bayes
  • 3,234
  • 2
  • 25
  • 20
Katya Pavlenko
  • 3,303
  • 3
  • 16
  • 28
  • 10
    I think the trick here is that `setState` will *always* trigger a re-render. Rather than checking `state.top` and calling `setState` multiple times, just track what you want `state.top` to be in a local variable, then once at the end of `componentDidUpdate` call `setState` only if your local variable doesn't match `state.top`. As it stands right now, you immediately reset `state.top` after the first re-render, which puts you in the infinite loop. – Randy Morris May 29 '15 at 13:08
  • 3
    See the two different implementations of `componentDidUpdate` in [this fiddle](https://jsfiddle.net/69z2wepo/9252/). – Randy Morris May 29 '15 at 13:24
  • 1
    damn it! local variable solves the whole problem, how hadn't i figured it out by mysef! Thank you! – Katya Pavlenko May 29 '15 at 16:07
  • 1
    I think you should accept the answer below. If you read it again I think you'll find it does answer the initial question sufficiently. – Randy Morris May 29 '15 at 16:10
  • Why has no one suggested moving the condition into `componentShouldUpdate`? – Patrick Roberts Jul 20 '16 at 04:19
  • @KatyaPavlenko thanks for the local variable suggestion (instead of state) – Manohar Reddy Poreddy May 26 '21 at 13:27

9 Answers9

138

You can use setStateinside componentDidUpdate. The problem is that somehow you are creating an infinite loop because there's no break condition.

Based on the fact that you need values that are provided by the browser once the component is rendered, I think your approach about using componentDidUpdate is correct, it just needs better handling of the condition that triggers the setState.

damianmr
  • 2,511
  • 1
  • 15
  • 15
  • 7
    what do you mean by 'break condition'? checking if state is already set and not resetting it? – Katya Pavlenko May 29 '15 at 12:43
  • I agree with this, my only additional comment would be that the adding/removing of classes is probably unnecessary in `componentDidUpdate` and can just be added as needed in `render` instead. – Randy Morris May 29 '15 at 12:44
  • but class adding/removing depends on dropdown position which is checked in in componentDidUpdate, you suggest to check it twice? And as i understand, componentDidUpdate is called AFTER render(), so it's useless to add/remove class in render() – Katya Pavlenko May 29 '15 at 12:56
  • i have added my code with setstate, can you check it and point me to my mistake? or show me some example which wouldn't cause loop – Katya Pavlenko May 29 '15 at 13:00
  • 3
    componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } } – Ashok R Oct 12 '18 at 11:28
95

The componentDidUpdate signature is void::componentDidUpdate(previousProps, previousState). With this you will be able to test which props/state are dirty and call setState accordingly.

Example:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}
Ninjakannon
  • 3,751
  • 7
  • 53
  • 76
Abdennour TOUMI
  • 87,526
  • 38
  • 249
  • 254
  • `componentDidMount` doesn't have any arguments, and is only called when the component is created, so can't be used for the described purpose. – Jules Sep 07 '17 at 07:34
  • @Jules Thanks ! I used to write `componentDidMount` , so when I wrote the answer the famous name cascaded Again, Thanks & Great catch up! – Abdennour TOUMI Sep 07 '17 at 08:41
  • ```componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } } ``` – Ashok R Oct 12 '18 at 11:28
  • I know your concern @AshokR . You reduce the arg name. but "prev" can mean prevent not previous.. hhh . .kidding :) – Abdennour TOUMI Apr 16 '20 at 17:01
62

If you use setState inside componentDidUpdate it updates the component, resulting in a call to componentDidUpdate which subsequently calls setState again resulting in the infinite loop. You should conditionally call setState and ensure that the condition violating the call occurs eventually e.g:

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

In case you are only updating the component by sending props to it(it is not being updated by setState, except for the case inside componentDidUpdate), you can call setState inside componentWillReceiveProps instead of componentDidUpdate.

Ninjakannon
  • 3,751
  • 7
  • 53
  • 76
mickeymoon
  • 4,820
  • 5
  • 31
  • 56
8

This example will help you to understand the React Life Cycle Hooks.

You can setState in getDerivedStateFromProps method i.e. static and trigger the method after props change in componentDidUpdate.

In componentDidUpdate you will get 3rd param which returns from getSnapshotBeforeUpdate.

You can check this codesandbox link

// 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>
Nikhil Mahirrao
  • 3,547
  • 1
  • 25
  • 20
2

I would say that you need to check if the state already has the same value you are trying to set. If it's the same, there is no point to set state again for the same value.

Make sure to set your state like this:

let top = newValue /*true or false*/
if(top !== this.state.top){
    this.setState({top});
}
gradosevic
  • 4,809
  • 2
  • 36
  • 51
0

this.setState creates an infinite loop when used in ComponentDidUpdate when there is no break condition in the loop. You can use redux to set a variable true in the if statement and then in the condition set the variable false then it will work.

Something like this.

if(this.props.route.params.resetFields){

        this.props.route.params.resetFields = false;
        this.setState({broadcastMembersCount: 0,isLinkAttached: false,attachedAffiliatedLink:false,affilatedText: 'add your affiliate link'});
        this.resetSelectedContactAndGroups();
        this.hideNext = false;
        this.initialValue_1 = 140;
        this.initialValue_2 = 140;
        this.height = 20
    }
0

I faced similar issue. Please make componentDidUpdate an arrow function. That should work.

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});
       }
    }
}
hrushi
  • 1
-1

I had a similar problem where i have to center the toolTip. React setState in componentDidUpdate did put me in infinite loop, i tried condition it worked. But i found using in ref callback gave me simpler and clean solution, if you use inline function for ref callback you will face the null problem for every component update. So use function reference in ref callback and set the state there, which will initiate the re-render

Sanjay
  • 467
  • 5
  • 6
-2

You can use setState inside componentDidUpdate

Vijay
  • 275
  • 2
  • 11