Acy
Acy

Reputation: 659

How to wait for data to finish fetching with async/await in React

My own answer to the question works, Pedrag's comment which is included in my answer also works

I am doing a http request call and don't want to display the page until the http response came back (the request call is getRoadmaps). I have a redux state called "loading" to determine if the response came back yet? Any insights on how to implement that in my code? My other redux states are roadmap, which contains an object also called roadmap (sorry for the confusion).

P.S. I saw a few answers that said to do (psuedo code): if not loaded display loading screen else load the normal screen in render. But my problem doesn't exists in render, but in componentWillMount() where I do

this.props.getRoadmaps() 

and then 3 lines later where I do

console.log(this.displayRoadmap[0].roadmap[0])

which should successfully log the roadmap if getRoadmaps() finished, it seems like getRoadmaps() is called but then the program continues without getRoadmaps() completely finishes, which causes my displayRoadmap to be undefined. This also leads to some weird phenomenon like if I go from one screen into this component it always works but if I refresh the page it doesn't work

import React, { Component } from "react";
import Tree from "./Tree";
import TreeSidebar from "./TreeSidebar";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { getRoadmaps } from "../actions/roadmapAction";
class OneRoadmap extends Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: this.props.roadmap.loading,
      roadmapName: "click a white circle to show more!",
      roadmapImg:
        "https://cdn3.iconfinder.com/data/icons/harmonicons-06/64/plus-circle-512.png",
      roadmapDetail: [{ text: "Click on something" }, { text: "hi" }],
      treeData: //deleted,just the default big json object
   };
  }

  componentWillMount() {
    this.props.getRoadmaps();

    var location1 = this.props.location.pathname;
    var n = location1.slice(9);

    var current_roadmaps = this.props.roadmap.roadmaps;

    this.displayRoadmap = current_roadmaps.filter(
      eachRoadmap => eachRoadmap._id == n
    );
    // now we have a roadmap
    console.log(this.displayRoadmap[0].roadmap[0]);
    this.setState({ treeData: this.displayRoadmap[0].roadmap[0] });
    console.log("is good");
    console.log(this.props.loading);
  }

  componentDidMount() {}

  handle_on_click_change = d => {
    this.setState({ roadmapName: d.data.name });
    this.setState({ roadmapImg: d.data.img });
    if (d.data.details == undefined) {
      this.setState({
        roadmapDetail: [{ text: "The author did not put anything here" }]
      });
    } else {
      this.setState({ roadmapDetail: d.data.details });
    }
  };

  render() {
    return (
      <div>
        {console.log(this.state.loading)}
        <ul>
          <li styles={{ float: "left" }}>
            <div>
              {console.log(this.state.treeData)}
              <Tree
                on_click_change={this.handle_on_click_change}
                roadmapData={this.state.treeData}
              />
            </div>
          </li>
          <li styles={{ float: "right" }}>
            <div>
              <TreeSidebar
                displayName={this.state.roadmapName}
                displayImg={this.state.roadmapImg}
                displayDetail={this.state.roadmapDetail}
              />
            </div>
          </li>
        </ul>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  roadmap: state.roadmap
});

export default connect(
  mapStateToProps,
  { getRoadmaps }
)(OneRoadmap);

Upvotes: 1

Views: 19536

Answers (4)

elraphty
elraphty

Reputation: 630

Personally, how I do this I use CSS styles I don't know if this if the best approach to do this but it works for me, I will do something like this

 this state = {
      loaderStyle: 'block',
      contentStyle: 'none'
 }
componentDidMount() {
    If(this.displayRoadmap[0].length > 0) {
       this.setsState({loaderStyle: 'none', contentStyle: 
          'block:
    } else /// do something

};

In my render function, I will do this

 <Loader style={{display: this.state.loaderStyle}}/>

 <Content style={{display: this.state.contentStyle}}>. 
 </Content>

Upvotes: 4

Acy
Acy

Reputation: 659

I checked elraphty's answer because he does answer part of my question and he also wanted me to check his answer. Anyways, if you have trouble fetching data and waiting for the data to finish fetching, here's a complete example of how I do it. Predrag Beocanin's comment works:

fetch('some_url', { method: 'GET' }).then(res = res.json()).then(data => { // some parse here; return parsed}

Here's how I do it (using axios instead of metch, sending request to another completely different file /api/roadmap, and then using Redux to update the store.

In OneRoadmap.js, which is the component I need data fetching in, this is what I call to fetch the data:

  async componentDidMount() {
await this.props.getRoadmaps();
.....
}

This makes it so that we will run this.props.getRoadmaps() and wait until the line to complete before going to the next line. If you don't put async/await, this.props.getRoadmaps() will trigger but the code in componentDidMount() will immediately continue without this.props.getRoadmaps() to finish data fetching and then updating the state. Now, ofcourse, Predrag's answer should definitely solve this problem, but just in case you like to place data fetching and state updating with redux in different files (like me), keep reading.

this is what getRoadmaps() does, which is in a file called RoadmapActions.js:

    export const getRoadmaps = () => dispatch => {
  dispatch(setRoadmapsLoading());
  return axios
    .get("/api/roadmaps")
    .then(res => dispatch({ type: GET_ROADMAPS, payload: res.data }));
};

This says that we will send a get request to "/api/roadmaps", then take the return data from the get request, and then send a dispatch to the redux store, which will update the store state and we will ultimately use it in OneRoadmap.js. This is what /api/roadmaps get request returns:

router.get("/", (req, res) => {
Roadmap.find()
.sort({ date: -1 })
.then(roadmaps => res.json(roadmaps));
});

Roadmap.find() goes into our mongo Database and retrieves all the roadmap items, then we return that to whereever it was called from.

With that returned data, we send a dispatch to the reducer with that data, this is what GET_ROADMAPS() does, GET_ROADMAPS locates in roadmapReducer.js:

const initialState = {
  roadmaps: [],
  loading: false,
  current_roadmap: ""
};

export default function(state = initialState, action) {


switch (action.type) {
    case GET_ROADMAPS:
      return {
        ...state,
        roadmaps: action.payload,
        loading: false
      };


}

Then, that's it, your redux store state is updated and since I connected OneRoadmap.js with state.roadmap, I can use it freely in the code.

Upvotes: 0

Janith Widarshana
Janith Widarshana

Reputation: 3483

This can easily achieved by React Higher Order Components as follows.

withLoadingScreen.js

import * as React from "react";

const withLoadingScreen = WrappedComponent => {
  return class LoadingScreen extends React.Component {
    render() {
      if (this.props.loading) return <div className="pre-loader" > Loading... </div>
      return <WrappedComponent {...this.props} />;
    }
  };
};

export default withLoadingScreen;

Use HOC as follows with your component

export default compose(
  connect(mapStateToProps, mapActionCreators),
  withLoadingScreen
)(OneRoadmap)

You can pass custom parameters too to the HOC. For more example please follow links below. Make changes as you want on your code by referring them.

https://medium.com/@peterpme/learning-higher-order-components-in-react-by-building-a-loading-screen-9f705b89f569

https://medium.com/@Farzad_YZ/handle-loadings-in-react-by-using-higher-order-components-2ee8de9c3deb

Upvotes: 0

Predrag Beocanin
Predrag Beocanin

Reputation: 1400

First and most important, don't use componentWillMount, rather, use componentDidMount. UNSAFE_componentWillMount() is legacy and will be removed in version 17. Now, onto the next issue, you want to start your page off by setting the loading state to true.

this.state = {
      loading: true,
      roadmapName: "click a white circle to show more!",
      roadmapImg:
        "https://cdn3.iconfinder.com/data/icons/harmonicons-06/64/plus-circle-512.png",
      roadmapDetail: [{ text: "Click on something" }, { text: "hi" }],
      treeData: //deleted,just the default big json object
   };

After that, you want to tweak your render method a bit to support conditional rendering:

render() {
        if (this.state.loading) {
            return <LoadingBar />;
        }
        return (
            <div>
                <ul>
                    <li styles={{ float: 'left' }}>
                        <div>
                            {console.log(this.state.treeData)}
                            <Tree
                                on_click_change={this.handle_on_click_change}
                                roadmapData={this.state.treeData}
                            />
                        </div>
                    </li>
                    <li styles={{ float: 'right' }}>
                        <div>
                            <TreeSidebar
                                displayName={this.state.roadmapName}
                                displayImg={this.state.roadmapImg}
                                displayDetail={this.state.roadmapDetail}
                            />
                        </div>
                    </li>
                </ul>
            </div>
        );
    }

Now, whenever your data is ready, you can simply do this.setState({ loading: false }), and the render method will return whatever is not the loading bar. So in your specific case:

this.setState({ treeData: this.displayRoadmap[0].roadmap[0], loading: false });

Upvotes: 1

Related Questions