Reputation: 6587
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
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
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