0

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?

riv
  • 6,846
  • 2
  • 34
  • 63
  • I know that you don't want to hear this but react is just a view, and you need something to manage your state. While you might be able to pull this off, long term you would be much better using Mobx, or Redux. – nzajt Dec 13 '17 at 21:57
  • may i ask what is the use case here? never had a need to call the same API method multiple times inside the same life-cycle method. – Sagiv b.g Dec 13 '17 at 22:03
  • @Sag1v the actual use case would be multiple calls to `componentWillReceiveProps`. I.e. if the component is a tooltip that requires loading, and the user keeps hovering over different items, he might end up with tooltip for item 1 showing while the cursor is over item 2. – riv Dec 13 '17 at 22:10

1 Answers1

1

You can solve this by keeping track of a pointer, and only save the state if the resolved pointer matches with the current pointer (then its the latest). Disregard the result otherwise.

That being set, this is not the best way to keep state in React. It's a view library after all. May be Redux could help you (or it might be too much, that is also possible, really depends on the use case).

If you'd want to keep it all in React though, you could do with something like this.

class Example extends Component {
  constructor(props) {
    super(props);
    this.state({value: 'Loading', currentPointer: 0});
  }

  componentDidMount() {
    this.setPromise(timedPromise("Hello 1", 3000));
    setTimeout(() => {
      this.setPromise(timedPromise("Hello 2", 1000));
    }, 1000);
  }

  setPromise(promise) {
    const newPointer = this.state.currentPointer + 1;
    this.setState({currentPointer: newPointer}, () => 
      promise.then(value => {
        if (this.state.currentPointer == newPointer) {
          this.setState({value});
        } else {
          console.log(`Pointer mismatch, got ${newPointer} but current was ${this.state.currentPointer}`);
        }
      })
    });
  }

  render() {
    return <div>{this.state.data.value}</div>;
  }
}
Rogier Slag
  • 526
  • 3
  • 7
  • I thought about this, but it also runs into race conditions. What if the promise resolves before the call to `setState({currentPointer: newPointer})`? As I mentioned in the question, the spec does not guarantee anything about the resolution order of `setState`. – riv Dec 13 '17 at 22:00
  • I've edited the example to use the callback of `this.setState()` so you are sure the state is actually modified before the promise chain is entered. More info is at https://stackoverflow.com/questions/42038590/when-to-use-react-setstate-callback – Rogier Slag Dec 13 '17 at 22:03
  • Ah this makes more sense, I forgot `setState` can take both a modifier and a callback (you could probably store the actual promise instead of a counter I think). – riv Dec 13 '17 at 22:12
  • Storing the promise is also fine if you use that as a comparison. Both would work (although I like the idea of eliminating the pointer value, which is basically meta information anyhow). The callback of setState can be really useful for projects where you just have 3 promises (for example) so going all Redux is a bit overboard ;) (Feel free to edit my answer btw) – Rogier Slag Dec 13 '17 at 22:14
  • why do you store the `currentPointer` in the `state` in the first place? It is completely irrelevant to everything except that one function. Especially it's irrelevant to the render cycle that get's triggered by that state change. How about storing it in `this._currentPointer`? – Thomas Dec 14 '17 at 12:26