简体   繁体   中英

ReactJS: I can not get the state to update with API data when the asynchronous API data fetch is completed

I am having a bit of an issue rendering components before the state is set to the data from a returned asynchronous API request. I have a fetch() method that fires off, returns data from an API, and then sets the state to this data. Here is that block of code that handles this:

class App extends Component {
  constructor() {
    super();
    this.state = {
      currentPrice: null,
    };
  }
    componentDidMount() {
        const getCurrentPrice = () => {
          const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';

          fetch(url).then(data => data.json())
          .then(currentPrice => {
            this.setState = ({
              currentPrice: currentPrice.bpi.USD.rate
            })
            console.log('API CALL', currentPrice.bpi.USD.rate);
          }).catch((error) => {
            console.log(error);
          })
        }
       getCurrentPrice();
    } 

You will notice the console.log('API CALL', currentPrice.bpi.USD.rate ) that I use to check if the API data is being returned, and it absolutely is. currentPrice.bpi.USD.rate returns an integer ( 2345.55 for example) right in the console as expected.

Great, so then I assumed that this.setState = ({ currentPrice: currentPrice.bpi.USD.rate }) should set the state without an issue, since this data was received back successfully.

So I now render the components like so:

render() {
    return (
      <div>
        <NavigationBar />
        <PriceOverview data={this.state.currentPrice}/>
      </div>
    );
  }
}
export default App;

With this, I was expecting to be able to access this data in my PriceOverview.js component like so: this.props.data

I have used console.log() to check this.props.data inside my PriceOverview.js component, and I am getting 'null' back as that is the default I set intially. The issue I am having is that the components render before the API fetch has ran it's course and updated the state with the returned data. So when App.js renders the PriceOverview.js component, it only passes currentPrice: null to it, because the asynchronous fetch() has not returned the data prior to rendering.

My confusion lies with this.setState . I have read that React will call render any time this.setState is called. So in my mind, once the fetch() request comes back, it calls this.setState and changes the state to the returned data. This in turn should cause a re-render and the new state data should be available. I would be lying if I didn't say I was confused here. I was assuming that once the fetch() returned, it would update the state with the requested data, and then that would trigger a re-render.

There has to be something obvious that I am missing here, but my inexperience leaves me alone.. cold.. in the dark throws of despair. I don't have an issue working with 'hard coded' data, as I can pass that around just fine without worry of when it returns. For example, if I set the state in App.js to this.state = { currentPrice: [254.55] } , then I can access it in PriceOverview.js via this.props.data with zero issue. It's the async API request that is getting me here, and I am afraid it has gotten the best of me tonight.

Here App.js in full:

import React, { Component } from 'react';
import './components/css/App.css';
import NavigationBar from './components/NavigationBar';
import PriceOverview from './components/PriceOverview';

class App extends Component {
  constructor() {
    super();
    this.state = {
      currentPrice: null,
    };
  }
  componentDidMount() {
    const getCurrentPrice = () => {
      const url = 'https://api.coindesk.com/v1/bpi/currentprice.json';

      fetch(url).then(data => data.json())
      .then(currentPrice => {
        this.setState = ({
          currentPrice: currentPrice.bpi.USD.rate
        })
        console.log('API CALL', currentPrice.bpi);
      }).catch((error) => {
        console.log(error);
      })
    }
    getCurrentPrice();
  }

render() {
    return (
      <div>
        <NavigationBar />
        <PriceOverview data={this.state.currentPrice}/>
      </div>
    );
  }
}
export default App;

Here is PriceOverview.js in full:

import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';

class PriceOverview extends Component {

    constructor(props) {
        super(props);
        this.state = {
            currentPrice: this.props.data
        }
    }

    render() {
     return (
          <div className="overviewBar">
            <div className="currentPrice panel">
                 { this.state.currentPrice != null ? <div className="price">{this.state.currentPrice}</div> : <div className="price">Loading...</div> }
            </div>
          </div>
    )
  }
}
export default PriceOverview;

Thank you in advance to any help, it's much appreciated.

The thing is constructor of any JS class is called only once. It is the render method that is called whenever you call this.setState .

So basically you are setting currentPrice to null for once and all in constructor and then accessing it using state so it will always be null.

Better approch would be using props.

You can do something like this in your PriceOverview.js .

import React, { Component } from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';

class PriceOverview extends Component {

constructor(props) {
    super(props);
    this.state = {

    }
}

render() {
 return (
      <div className="overviewBar">
        <div className="currentPrice panel">
             { this.props.data!= null ? <div className="price">{this.props.data}</div> : <div className="price">Loading...</div> }
        </div>
      </div>
    )
  }
}
export default PriceOverview;  

Or you can use react lifecycle method componentWillReceiveProps to update the state of PriceOverview.js

componentWillReceiveProps(nextProps) {
    this.setState({
        currentPrice:nextProps.data
    });
}  

render() {
 return (
      <div className="overviewBar">
        <div className="currentPrice panel">
         { this.state.currentPrice != null ? <div className="price">{this.state.currentPrice }</div> : <div className="price">Loading...</div> }
        </div>
      </div>
    )
  }
}

Ok First thing, when you're writting code on React the components that hold state are the class base components so ... What I see here is that you're creating two class base components so when you pass down props from your app class component to your PriceOverview wich is another class base component you're essentially doing nothing... Because when your constructor on your PriceOverview get call you're creating a new state on that Component and the previous state ( that's is the one you want to pass down) is being overwritten and that's why you're seem null when you want to display it. So it should work if you just change your PriveOverview component to a function base component ( or a dumb component). So this way when you pass down the state via props, you're displaying the correct state inside of your div. This is how would look like.

import React from 'react';
import './css/PriceOverview.css';
import bitcoinLogo from './assets/bitcoin.svg';

const PriceOverview = (data) => {
  return (
      <div className="overviewBar">
        <div className="currentPrice panel">
        //Im calling data here because that's the name you gave it as ref
        //No need to use 'this.props' you only use that to pass down props
          {data != null ? <div className="price"> 
          {data}</div> : <div className="price">Loading...</div> 
         }
        </div>
      </div>
            )
        }
   }
export default PriceOverview;

Whenever you're writing new components start always with function base components if you component is just returning markup in it and you need to pass some data go to his parent component update it (making the api calls there or setting the state there) and pass down the props you want to render via ref. Read the React docs as much as you can, hope this explanation was useful (my apologies in advance if you don't understand quite well 'cause of my grammar I've to work on that)

 this.setState ({
          currentPrice: currentPrice.bpi.USD.rate
        })

Do not put an = in this.setState

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM