Reputation: 8256
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.flag
is 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
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:
componentDidMount
.flag: this.props.flag
hasn't happened yet).componentDidMount
, setState
in componentDidMount
is
still under execution (flag: this.props.flag
hasn't happened yet).componentWillReceiveProps
.if
(!_.isEmpty(nextProps.flag) && !_.isEqual(flag, nextProps.flag))
in
componentWillReceiveProps
is executed (this.state.flag
is still
undefined). setState
inside
componentDidMount
and sets flag: this.props.flag
and executes
doSomething()
.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
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