nCardot
nCardot

Reputation: 6587

Why do I need to use setState callback to set state for second state item that relies on first item's setState finishing?

In this componentDidUpdate method, after performing setState to set quotes to what's returned from the fetch, I have to use the callback to perform setState a second time to set randomQuoteIndex to the result of calling randomQuoteIndex, which relies on this.state.quotes.length, i.e.:

componentDidMount() {
    fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
      // Takes a JSON response string and parses it into JS object
      .then(response => response.json())
      // state is set to quotes: quotes due to destructuring
      // Using setState callback since setState is asynchronous and need to make sure quotes is loaded before setting the randomQuoteIndex state since it depends on it
      .then(quotes => this.setState({ quotes }, () => {
        this.setState({
          randomQuoteIndex: this.randomQuoteIndex(),
          isDoneFetching: true
        })
      }))
  }

Why doesn't the code below work? Based on the selected answer to this question, I'm under the impression that the second item in setState won't be applied until after state is set for the first item. If I try this, I get an error "TypeError: Cannot read property 'quote' of undefined." (I read that setState is asynchronous and about when to use the callback but I'm having a hard time understanding what I read/how it applies in this case.)

  componentDidMount() {
    fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
      // Takes a JSON response string and parses it into JS object
      .then(response => response.json())
      // Using setState callback since setState is asynchronous and need to make sure quotes is loaded before setting the randomQuoteIndex state since it depends on it
      .then(quotes => this.setState({
          quotes,
          randomQuoteIndex: this.randomQuoteIndex(),
          isDoneFetching: true
        }));
  }

Here's the full component code (the working version):

import React, { Component } from 'react';
import './App.css';
import { random } from 'lodash';
import Button from './components/Button';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      quotes: [],
      randomQuoteIndex: null,
      isDoneFetching: false
    }
  }

  componentDidMount() {
    fetch('https://gist.githubusercontent.com/nataliecardot/0ca0878d2f0c4210e2ed87a5f6947ec7/raw/1802a693d02ea086817e46a42413c0df4c077e3b/quotes.json')
      // Takes a JSON response string and parses it into JS object
      .then(response => response.json())
      // state is set to quotes: quotes due to destructuring
      // Using setState callback since setState is asynchronous and need to make sure quotes is loaded before setting the randomQuoteIndex state since it depends on it
      .then(quotes => this.setState({ quotes }, () => {
        this.setState({
          randomQuoteIndex: this.randomQuoteIndex(),
          isDoneFetching: true
        })
      }))
  }

  get randomQuote() {
    return this.state.quotes[this.state.randomQuoteIndex];
  }

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

  render() {
    return (
      <div className="App" id="quote-box">
        {this.state.isDoneFetching ? this.randomQuote.quote : 'Loading...'}
        <Button
          buttonDisplayName="Next"
          clickHandler={this.blah}
        />
      </div>
    );
  }
}

export default App;

Upvotes: 3

Views: 102

Answers (2)

Ryan Jenkins
Ryan Jenkins

Reputation: 890

It's not that setState is asynchronous, it's a result of randomQuoteIndex being called before the state is set. This would be the case with or without asynchronous updating of state. Consider this slightly refactored version of componentDidMount:

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

This is functionally exactly the same as the version you posted in your question. Hopefully this highlights that this.randomQuoteIndex() is evaluated before this.setState(newState), and because setState has not been called yet, there is no state, which randomQuoteIndex relies on. When calling setState the argument must be evaluated before it can be passed to setState, so synchronous or not, the update has not happed at the point that randomQuoteIndex is being called.

The easy way to fix this is to make randomQuoteIndex take the list of quotes as an argument rather than pulling it out of the component state. Rewritten, the pair of methods might look like:

  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);
  }

Which only requires that setState be called once, and (potentially) saves you a re-render.

Upvotes: 1

hovnozrout
hovnozrout

Reputation: 121

Personally I don't really think that React's authors intention about callback in setState was to use it to call next setState. Why not to try something like @Icepickle mentioned:

function randomQuoteIndex(quotes) {
  return random(0, quotes.length - 1);
}
...
...
  .then(quotes => {
    this.setState({
      quotes,
      randomQuoteIndex: randomQuoteIndex(quotes),
      isDoneFetching: true
    })
  }))
...

you update state only once => making sure about you have always just one render cycle

Upvotes: 1

Related Questions