Searching for this question always brings up something that essentially boils down to
class Test extends Component {
componentDidMount() {
this.props.promise.then(value => this.setState({value}));
}
render() {
return <div>{this.state.value || "Loading"}</div>;
}
}
This works if (1) the component will not dismount any time soon, and (2) the promise never changes. Both of these create potential issues when trying to come up with a more general solution.
The first issue can be handled by adding a "guard" property, not a big deal.
When trying to fix the second issue, it is very easy to run into race conditions. Consider the following code:
const timedPromise = (value, delay) => new Promise((resolve, reject) => {
setTimeout(() => resolve(value), delay);
});
class Test extends React.Component {
constructor(props) {
super(props);
this.state = {value: "Loading..."};
}
componentWillMount() {
this.setPromise(timedPromise("Hello 1", 3000));
setTimeout(() => {
this.setPromise(timedPromise("Hello 2", 1000));
}, 1000);
}
setPromise(promise) {
promise.then(value => this.setState({value}));
}
render() {
return <div>{this.state.value}</div>;
}
}
ReactDOM.render(<Test/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
In this example, "Hello 1" will be displayed in the end, even though "Hello 2" request was initiated later. This is a very common problem in some interfaces, where the user makes multiple requests and ends up with incorrect state.
The best solution I could come up with was the following:
const timedPromise = (value, delay) => new Promise((resolve, reject) => {
setTimeout(() => resolve(value), delay);
});
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {data: {value: "Waiting..."}};
}
componentWillMount() {
this.setPromise(timedPromise("Hello 1", 3000));
setTimeout(() => {
this.setPromise(timedPromise("Hello 2", 1000));
}, 1000);
}
setPromise(promise) {
const data = {value: "Loading..."};
this.setState({data: data})
promise.then(value => {
data.value = value;
this.forceUpdate();
})
}
render() {
return <div>{this.state.data.value}</div>;
}
}
ReactDOM.render(<Test/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
This works, but it relies on forceUpdate
, which does not seem very elegant. I can't seem to come up with any way to solve it by storing some values in the state that would not run into other race conditions, since the spec does not guarantee the order of setState
resolutions.
Is there a better pattern for solving this seemingly common problem?