0

As was pointed out to me in another question, if I have a function call without an argument in setState, randomQuoteIndex(), and the function uses a state set in that setState, it's called before setState.

  componentDidMount() {
        fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
          .then(response => response.json())
          .then(quotes => this.setState({
              quotes,
              randomQuoteIndex: this.randomQuoteIndex(),
              isDoneFetching: true
            }));
  }

randomQuoteIndex() {
    return random(0, this.state.quotes.length - 1);
  }

This results in an error because the state of quotes isn't available at the time of randomQuoteIndex() call.

However, if I change randomQuoteIndex() to instead use a passed in quotes parameter, as below, it works.

componentDidMount() {
    fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
      .then(response => response.json())
      .then(quotes => this.setState({
          quotes,
          randomQuoteIndex: this.randomQuoteIndex(quotes),
          isDoneFetching: true
        }));
  }

  randomQuoteIndex(quotes) {
    return random(0, quotes.length - 1);
  }

This is not what I expected; I had assumed that the state of quotes would be available at the time randomQuoteIndex() is called since it was called within setState. Why is randomQuoteIndex() called before setState even though it's inside it?

nCardot
  • 5,992
  • 6
  • 47
  • 83
  • 1
    My guess is that you aren't using a `constructor` function. This means the the component starts to mount, does mount, then calls your `randomQuoteIndex` function which is looking for the state object that was never created. You can use your second snippet here or if you want to use the first, simply add a `constructor` that sets `this.state = { quotes: [""] }` – adr5240 Jul 09 '19 at 16:05
  • `is called since it was called within setState` , no it's not. Your creating an object literal that you then send to `setState` – Keith Jul 09 '19 at 16:07
  • I am using a constructor. – nCardot Jul 09 '19 at 16:28

2 Answers2

2

It is because setState is an async operation. You can still use your original function by using setState as a callback

componentDidMount() {
        fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
          .then(response => response.json())
          .then(quotes => this.setState({
              quotes,
              isDoneFetching: true
            }, () => {
             this.setState({
             randomQuoteIndex: this.randomQuoteIndex(),
        }); //this callback will be executed after your state is updated.
    }));

}

randomQuoteIndex() {
return random(0, this.state.quotes.length - 1);
}

So, essentially once a setState function is performed, the updated values will only reflect after the update lifecycle is finished, which means you can get the values either in componentDidUpdate method or by using the setState function as a callback.

I wouldn't recommend this solution but in this case, the function randomQuoteIndex() will get the updated state values without having any parameter passed.

Aseem Upadhyay
  • 4,279
  • 3
  • 16
  • 36
  • The answerer of my other question said it has nothing to do with it being an async function. I'm even more confused now. – nCardot Jul 09 '19 at 16:24
  • 1
    Natalie, This is the correct answer, setState is asynchronous. If you want to use the updated state, you need to do it in a callback of the setState function. I suggest reading https://reactjs.org/docs/react-component.html#setstate carefully. – plus- Jul 09 '19 at 16:29
  • Yes setState is async, but to me that doesn't explain why a function call inside setState wouldn't be applied at a different time that the other items in that setState. I read that section of the doc and didn't find an answer there. – nCardot Jul 09 '19 at 17:55
  • 1
    Because that is a function which is using variables synchronously and you're setting them asynchronously? – Aseem Upadhyay Jul 09 '19 at 18:53
1

Thats because at the time you passed the object to setState, you are executing inline call to this.randomQuoteIndex.

state.quotes did not exist yet in the state.

constructor() {
    this.state = { quotes: [] };

}

componentDidMount() {
    fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
      .then(response => response.json())
      .then(quotes => {

          this.setState({
            quotes,
            randomQuoteIndex: this.randomQuoteIndex(),
            isDoneFetching: true
        })
      });

  }

  randomQuoteIndex() {
    return random(0, this.state.quotes.length - 1);
  }
Ion
  • 1,262
  • 12
  • 19
  • That is what I thought was happening, but do you know why it happens that way? – nCardot Jul 09 '19 at 17:57
  • 1
    I can only tell you that is has to do with execution order, by the time the parser hits your this.randomQuoteIndex() line, it will execute, but it has more properties to check, so setState must wait until the parser goes though all properties. Did it help? – Ion Jul 09 '19 at 17:59
  • If you want to dig more into why this is so, I can recommend you the book (dont mind the title) You Dont Know JS, it goes deep in these kind of topics. – Ion Jul 09 '19 at 18:01