CiccioMiami
CiccioMiami

Reputation: 8256

Why in React componentWillReceiveprops fires before setState() in componentDidMount?

I have been programming with React for a while now but I have never faced this annoying issue, in one of my components componentWillReceiveProps fires before setState() in componentDidMount gets executed. This causes several issues in my application.

I have a variable this.props.flag received from props which is going to be stored in the state of the component:

    componentDidMount() {
        if (!_.isEmpty(this.props.flag)) {

            console.log('Flag Did:', this.props.flag);

            this.setState({
                flag: this.props.flag
            },
                () => doSomething()
            );
    }

In my componentWillReceiveProps method the variable this.state.flag is going to be replaced just if it is empty or if it different from the value of this.props.flag (the checks are made by using the lodash library):

componentWillReceiveProps(nextProps) {
    const { flag } = this.state;

    console.log('Flag Will:', !_.isEqual(flag, nextProps.flag), flag, nextProps.flag);

    if (!_.isEmpty(nextProps.flag) && !_.isEqual(flag, nextProps.flag)) {
            this.setState({
                flag: nextProps.flag,
            },
                () => doSomething()
            );
    }
}

Suppose that the prop flag in this case has always the same value and that this.state.flag is initialized to undefined. When I check the console log I see the following result:

Flag Did: true
Flag Will: true undefined true

Therefore when the code enters componentWillReceiveProps the value of this.state.flagis still undefined, that means has not been set yet by the setState in componentDidMount.

This is not consistent with React lifecycle or am I missing something? How can I avoid such behaviour?

Upvotes: 1

Views: 815

Answers (2)

CiccioMiami
CiccioMiami

Reputation: 8256

As suggested by user JJJ, given the asynchronous nature of setState, the check if (!_.isEmpty(nextProps.flag) && !_.isEqual(flag, nextProps.flag)) in componentWillReceiveProps is executed before setState inside componentDidMount executes flag: this.props.flag. The order of the operations is:

  1. Code enters componentDidMount.
  2. Code executes setState in componentDidMount (flag: this.props.flag hasn't happened yet).
  3. Code exits componentDidMount, setState in componentDidMount is still under execution (flag: this.props.flag hasn't happened yet).
  4. Component receive new props, therefore enters componentWillReceiveProps.
  5. The statement if (!_.isEmpty(nextProps.flag) && !_.isEqual(flag, nextProps.flag)) in componentWillReceiveProps is executed (this.state.flag is still undefined).
  6. Code finishes the execution of setState inside componentDidMount and sets flag: this.props.flag and executes doSomething().
  7. Code finishes the execution of setState inside componentWillMount and sets flag: nextProps.flag and executes doSomething().

Given the asynchronous nature of setState, 6 and 7 could be executed in parallel and therefore we do not know which one will finish its execution first. DoSomething() in this case is potentially called at least 2 times when it must be called once instead.

In order to solve these issues, I changed my code this way:

componentWillReceiveProps(nextProps)  {

if (!_.isEmpty(nextProps.flag) && !_.isEqual(this.props.flag, nextProps.flag)) {
        this.setState({
            flag: nextProps.flag,
        },
            () => doSomething()
        );
    }
}

This way I compare the new version(nextProps) with the old version(this.props) of the props, without waiting for the flag value to be stored in the component's state.

Upvotes: 0

Sakhi Mansoor
Sakhi Mansoor

Reputation: 8102

ComponentWillReceiveProps() will be called in each update life-cycle caused by changes to props (parent component re-rendering). Since Javascript is synchronous you might have validate props sometimes to save app crashes. I've not totally understood the context of your app but what you can do is:

componentWillReceiveProps(nextProps) {
    const { flag } = this.state;

    if(!flag){
      return;, 
    }

    console.log('Flag Will:', !_.isEqual(flag, nextProps.flag), flag, nextProps.flag);

    if (!_.isEmpty(nextProps.flag) && !_.isEqual(flag, nextProps.flag)) {
            this.setState({
                flag: nextProps.flag,
            },
                () => doSomething()
            );
    }
}

You can return if state is undefined. It will be called again upon parent re-rendering. But this might not be use-case.

Anyways you should look into this:

But I can think of at least 1 (maybe theoretical) scenario where the order will reversed:

Component receives props, and starts rendering. While component is rendering, but has not yet finished rendering, component receives new props. componentWillReceiveProps() is fired, (but componentDidMount has not yet fired) After all children and component itself have finished rendering, componentDidMount() will fire. So componentDidMount() is not a good place to initialise component-variables like your { foo: 'bar' }. componentWillMount() would be a better lifecycle event. However, I would discourage any use of component-wide variables inside react components, and stick to design principles:

all component variables should live in either state or props (and be immutable) all other variables are bound by the lifecycle method (and not beyond that)

Upvotes: 1

Related Questions