If props is updated, then the update must come from parent component.
That is correct.
If the update comes from a parent component, why is it possibly asynchronous?
You can pass parent's setState()
with props to a child and call it somewhere in child's code. And it still may be asynchronous. I came up with the following example:
class Container extends React.Component {
state = { containerState: 0 };
render() {
return (
<Component
containerState={this.state.containerState}
setContainerState={this.setState.bind(this)}
/>
);
}
}
And then, in child Component
, you have code like this:
this.props.setContainerState({
containerState: this.props.containerState + 1
});
// You want to use updated this.props.containerState here, but you can't,
// because parent's setState() MAY BE deferred
this.props.setState({ componentStateUpdatedWithObject: this.props.containerState });
// Now if you use the function instead, you can expect to get updated props
// as a second argument
this.props.setState((state, props) => ({
componentStateUpdatedWithFunction: props.containerState
}));
See the full code for my example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
<script
src="https://unpkg.com/react@16/umd/react.development.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"
crossorigin
></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
class Container extends React.Component {
state = { containerState: 0 };
render() {
return (
<Component
containerState={this.state.containerState}
setContainerState={this.setState.bind(this)}
/>
);
}
}
class Component extends React.Component {
state = {
componentStateUpdatedWithObject: [0, this.props.containerState],
componentStateUpdatedWithFunction: [0, this.props.containerState]
};
log = title => {
console.log(title);
console.log("- containerState", this.props.containerState);
console.log(
"- componentStateUpdatedWithObject",
this.state.componentStateUpdatedWithObject
);
console.log(
"- componentStateUpdatedWithFunction",
this.state.componentStateUpdatedWithFunction
);
console.log("==========");
};
update = () => {
this.log("before update");
this.props.setContainerState({
containerState: this.props.containerState + 1
});
this.log("after setContainerState");
this.setState({
componentStateUpdatedWithObject: [
this.state.componentStateUpdatedWithObject[0] + 1,
this.props.containerState
]
});
this.log("after setState with object");
this.setState((state, props) => ({
componentStateUpdatedWithFunction: [
state.componentStateUpdatedWithFunction[0] + 1,
props.containerState
]
}));
this.log("after setState with function");
};
componentDidMount() {
// setInterval(this.update, 2000);
}
render() {
this.log("on render");
console.log("---------------------------------------------");
return (
<div>
<div>containerState: {this.props.containerState}</div>
<div>
componentStateUpdatedWithObject:{" "}
{this.state.componentStateUpdatedWithObject.join(", ")}
</div>
<div>
componentStateUpdatedWithFunction:{" "}
{this.state.componentStateUpdatedWithFunction.join(", ")}
</div>
<button onClick={this.update}>UPDATE</button>
</div>
);
}
}
ReactDOM.render(<Container />, document.getElementById("root"));
</script>
</html>
NOTE: Not all setState()
calls are asynchronous. In my example if you uncomment setInterval(this.update, 2000)
in the componentDidMount
, you won't get the same behavior. This way setState()
calls are synchronous.
See this link for explanation why this happens: When and why are setState() calls batched?
In short, this is because
Currently (React 16 and earlier), only updates inside React event handlers are batched by default. There is an unstable API to force batching outside of event handlers for rare cases when you need it.