2

I have a project which does not support generators and async await syntax.

I built the following code with async await because I don't see any other way to do it:

 this.setState(async lastState => {
      const newImagesAmount = lastState.images.length + 20;
      const newImages = await this.getImages(this.props.tag, newImagesAmount);
      return {
        images: newImages
      };
    });

Why? In this particular case, the new state is built by both the old state and a result of a promise.

How can I transform it to non - async await syntax?


Note (Update):

Due to the fact that both the current answers contain the same bug, please read @dhilt answer + responses first which explain what are the bugs.

Stav Alfi
  • 13,139
  • 23
  • 99
  • 171

4 Answers4

1

If the code in the question is correct and you just want to purge async/await in favour of traditional return promise.then(...) syntax, then do as follows:

this.setState(lastState => {
    const newImagesAmount = lastState.images.length + 20;
    return this.getImages(this.props.tag, newImagesAmount)
    .then(newImages => ({ 'images': newImages });
});

The following simplification may also work :

this.setState(lastState => {
    return this.getImages(this.props.tag, lastState.images.length + 20)
    .then(images => ({ images });
});
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Can I return a promise from setState? – Stav Alfi Aug 24 '18 at 16:54
  • Please remember, I haven't seen `setState()` just an example of how it is called. – Roamer-1888 Aug 24 '18 at 16:58
  • I'm sorry, I didn't understand what you were trying to say. Is your code valid in react? – Stav Alfi Aug 24 '18 at 17:01
  • I have no idea. – Roamer-1888 Aug 24 '18 at 17:02
  • 1
    This answer gives the OP exactly what he asked for - "How can I transform it to non - async await syntax?" so Downvoter, please have the courage of your convictions and post a comment to support your criticism. If you can spot some way in which the answer can be improved, then I will be pleased to do so. – Roamer-1888 Aug 24 '18 at 17:46
  • One of my tags is `react`. This is a react question and not a general transpiler question. – Stav Alfi Aug 24 '18 at 18:18
  • Yes, I noticed the react tag but why suddenly introduces transpilers? React is a lib not a transpiler. As far as I can tell this question has nothing to do with transpilation. – Roamer-1888 Aug 24 '18 at 18:27
1

First of all it's impossible to use await inside of setState(). Not because of your project's limitation but because each function with await inside should be declared as async beforehand. And it should be invoked with await notation or with .then() chained. So callback passed into .setState should be async and called with await but we know that React does not work this way. React expect ordinary function and call it without await or .then(). It looks like func.apply(context, funcArgs) in react-dom module. Also there is no check if it returns Promise or not.

So let's return back to your final goal. As far as I understand it's about ensuring state is still consistent after deferred data is loaded. And here is another limitation. Except sync XHR(that is really bad practice) there is no way to pause all the execution until data comes. That is: whatever you do you cannot be 100% sure state has not mutate between "request has been sent" and "data is received" steps. Neither await, nor functional setState does not allow freeze execution. Yes, your code is still referencing to "prevState" variable but it could be not actual component's state anymore!

So what you could do:

  1. abort ongoing XHR before sending another one to the same endpoint(should work for read operations; does not make sense for write operations)
  2. parse request params when processing response to inject it into state in valid way(say, first response is for 1..10 images and next one is for 11..20 - so you can work with that and inject second response into appropriate indexes leaving first 10 still unintialized)
  3. set up some flag to block new requests until ongoing is finished

P.S. so making request from inside setState function leads to confusion and don't add any additional flexibility.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • Are you saying that no side-effects should be done from inside the setState function? Well, my side-effects are always defined __by__ the old state. Should I move all those states to redux and only call setState with the final state of redux? – Stav Alfi Sep 12 '18 at 18:05
  • yes, side effects and working with async operations inside make things worse. just await until request is fullfilled and only then either dispatch redux action or call `.setState`. and even then there is challenge I described at the end(to ensure your response has come for most recent request). – skyboyer Sep 12 '18 at 18:14
  • as for question 'should it be redux or .setState' I cannot really answer. It depends. afaik redux motivates to keep all the application state in one place but there definitely exceptions exists. Say if you are loading autocomplete suggestions for several input fields at the page you better not moving all these dozens of independant lists into single application store. – skyboyer Sep 12 '18 at 18:16
0

I think you may want something like this

const newImagesAmount = this.state.images.length + 20;
this.getImages(this.props.tag, newImagesAmount).then(newImages => 
  this.setState(prevState => ({
      ..prevState,
      images: newImages
    })
);

Firstly, you do a request for new images and this request is based on current state. Then (when the result is arrived) you update state.


Updated version that allows to protect async logic from multiple executions before the request is finished

if (!this.state.pending) {
  this.setState(lastState => {
    const newImagesAmount = lastState.images.length + 20;
    this.getImages(this.props.tag, newImagesAmount).then(newImages => 
      this.setState(prevState => ({
          ...prevState,
          pending: false,
          images: newImages
        })
    ).catch(() =>
       this.setState(prevState => ({
          ...prevState,
          pending: false
        })
    );
    return {
      ...lastState,
      pending: true
    }
  });
}
Stav Alfi
  • 13,139
  • 23
  • 99
  • 171
dhilt
  • 18,707
  • 8
  • 70
  • 85
  • It may cause bugs. Please read: https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b – Stav Alfi Aug 23 '18 at 13:58
  • 1
    actually in this case you don't need functional version of setState. Just set up `images` and that's it – skyboyer Aug 23 '18 at 14:14
  • 2
    @StavAlfi could you please specify what do you mean? if you anticipate that this.state can be changed while request to the server is ongoing - then you don't have a way to prevent that. `batch update` also would not affect the logic since you don't operate on `this.state` inside of `this.setState`... do you mean something else? – skyboyer Aug 23 '18 at 14:17
  • @StavAlfi What issues do you expect with such an approach? – dhilt Aug 23 '18 at 14:18
  • if I execute multiple times the same code without the setState to execute after each time, then your first line in each execution will have the same value. That's why it must be calculated from the prevState by giving a function to setState. – Stav Alfi Aug 23 '18 at 14:37
  • @StavAlfi I've updated the answer by adding a new version that could be helpful from the point you are worrying about. – dhilt Aug 23 '18 at 14:53
  • Thanks but it still has the same problem because if there may be a situation where the first `setState` is executed and so `newImagesAmount` will have the same value because the second setState didn't execute yet. Thats a bug and not something I'm worrying about. At least from the formal definition of a bug. – Stav Alfi Aug 23 '18 at 14:59
  • @StavAlfi I'm expecting the first setState won't be triggered after pending is `true` and before the second setState resets it to `false`. – dhilt Aug 23 '18 at 15:10
  • Yeah, I got you but because setState is async and because this code is executed after use does something like clicking than if he does it fast, he will pass the `if` in case the first setState didn't invoke yet. – Stav Alfi Aug 23 '18 at 15:13
  • @StavAlfi Okay! here we may provide one more state-wrapper handling new state.clicked flag or for example implement some kind of [debounced event handler](https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js) or something else. Anyway, I guess it is a good theme for new SO question. If you will post a new question, add a link here, please, I want to follow. – dhilt Aug 23 '18 at 15:29
  • I will let you know after I will read about it. I also advise you to update both of your codes and explain why they may not work as expected for others to read about it. It's extremely important due to the fact that both of the provided answers are the same. – Stav Alfi Aug 23 '18 at 15:40
  • @skyboyer You understood the problem perfectly well. If you say that there is no solution, please post an answer. Also, if you think that async await may also not work sometimes, please tell me also. Thank you! – Stav Alfi Aug 23 '18 at 15:45
0

Im assuming that lastState here is really just the current state before you update it. Try something like this:

const imageLength = this.state.images.length + 20
this.getImages(this.props.tag, imageLength)
     .then(newImages => {
     this.setState({images: newImages})
}
tallpaulk
  • 164
  • 1
  • 1
  • 11
  • It may cause bugs. Please read: https://medium.freecodecamp.org/functional-setstate-is-the-future-of-react-374f30401b6b – Stav Alfi Aug 23 '18 at 14:04
  • What bugs do you expect to see with my method? – tallpaulk Aug 23 '18 at 14:26
  • if I execute multiple times the same code without the setState to execute after each time, then your first line in each execution will have the same value. That's why it must be calculated from the prevState by giving a function to setState. – Stav Alfi Aug 23 '18 at 14:38