11

I am attempting to pull data from Open Data to put together a quick heat map. In the process, I want to add some stats. Almost everything runs well in that I have the data and am able to render the map, but I am unsure how to deal with calculations once I get the data since it takes time for data to come in. How do I set things up so that I can run a function on a state variable if it hasn't necessarily received data yet? Currently I am getting a null as the number that is passed as props to StatCard.

Below are my attempts:

App.js

import React, { Component } from 'react';
import Leaf from './Leaf';
import Dates from './Dates';
import StatCard from './StatCard';
import classes from './app.module.css';

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      data:[],
      cleanData:[],
      dateInput: '2019-10-01',
      loading: false,
      totalInspections: null,
      calculate: false
    };
  }

  componentDidMount() {
    try {
      this.fetchData();
    } catch (err) {
      console.log(err);
      this.setState({
        loading: false
      })
    }
  }


  fetchData=()=>{
    const requestData = async () => {
      await fetch(`https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=latitude > 39 AND latitude< 45 AND inspection_date >= '${this.state.dateInput}'&$limit=50000`)
        .then(res => res.json())
        .then(res =>
          //console.log(res)
          this.setState({ data: res, loading: true})
        )
    }

    const  calculateInspections = () => {
      this.setState({totalInspections: this.state.data.length})
    }

    //call the function
    requestData();

    if(this.state.data) {
      calculateInspections();
    }
  }

  handleDateInput = (e) => {
    console.log(e.target.value);
    this.setState({dateInput:e.target.value, loading: false}) //update state with the new date value
    this.updateData();
    //this.processGraph(e.target.value)
  }

  updateData =() => {
    this.fetchData();
  }

  LoadingMessage=()=> {
    return (
      <div className={classes.splash_screen}>
        <div className={classes.loader}></div>
      </div>
    );
  }


  //inspection_date >= '${this.state.dateInput}'& 
 // https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=inspection_date >= '2019-10-10T12:00:00' 

  render() {



    return (
      <div>

        <div>{!this.state.loading ? 
              this.LoadingMessage() : 
              <div></div>}
        </div>
          
        {this.state.totalInspections && <StatCard totalInspections={this.state.totalInspections} /> }
          
          <Dates handleDateInput={this.handleDateInput}/>
          <Leaf data={this.state.data} />
          
      </div>
    );
  }
}

export default App;

StatCard.js

import React from 'react';


const StatCard = ( props ) => {
    
    return (
        <div >
            { `Total Inspections: ${props.totalInspections}`}
        </div>
    )
};

export default StatCard;

Attempt Repair

   componentDidMount() {
    try {
      this.fetchData();
    } catch (err) {
      console.log(err);
      this.setState({
        loading: false
      })
    }
  }


  componentDidUpdate () {
    if(this.state.data) {
      this.setState({totalInspections: this.state.data.length})
    }
  }

  fetchData= async ()=>{
    const requestData = () => {
    fetch(`https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=latitude > 39 AND latitude< 45 AND inspection_date >= '${this.state.dateInput}'&$limit=50000`)
        .then(res => res.json())
        .then(res =>
          //console.log(res)
          this.setState({ data: res, loading: true})
        )
    }
    //call the function
    await requestData();
   
  }
starball
  • 20,030
  • 7
  • 43
  • 238
LoF10
  • 1,907
  • 1
  • 23
  • 64
  • Is `this.setState({ data: res, loading: true})` a typo in the `requestData` function? shouldn't `loading` be set to `false` when the data is fetched? – thgaskell Nov 15 '19 at 23:46
  • yeah its a typo – LoF10 Nov 15 '19 at 23:59
  • Are you sure the use of query parameters is corresponding with the API documentation? – Jurrian Nov 18 '19 at 18:52
  • Yes, as you can see in the code the data is fetched, that is not the problem. I have the data. The issue is I am unsure where and when to run calculateInspections() because it needs to run when I have received all of the API data and then be passed as props to the statCard. – LoF10 Nov 18 '19 at 19:04
  • Please send me the codesandbox link – TopW3 Nov 24 '19 at 21:32

5 Answers5

1

So your problem is that isLoading state needs to be set synchronously before any async calls.

So in your componentDidMount:

componentDidMount() {
    try {
      this.setState({ loading: true }); // YOU NEED TO SET TRUE HERE
      this.fetchData();
    } catch (err) {
      console.log(err);
      this.setState({
        loading: false
      })
    }
}

This ensures loading as soon as you make the call. Then your call is made and that part is asynchronous. As soon as data comes through, the loading is done:

.then(data => {
  this.setState({
    data: data,
    loading: false, // THIS NEEDS TO BE FALSE
    totalInspections: this.state.data.length
  })
})

Furthermore, your render method can have multiple return statements. Instead of having conditional JSX, return your loading layout:

render() {
    if (this.state.loading) {
        return <div> I am loading </div>
    }

    return <div> Proper Content </div>;
}
tdy
  • 36,675
  • 19
  • 86
  • 83
Daniel Duong
  • 1,084
  • 4
  • 11
0

Only render <StatCard /> if you have the data you need:

{this.state.totalInspections && <StatCard totalInspections={this.state.totalInspections} /> }
Chase DeAnda
  • 15,963
  • 3
  • 30
  • 41
  • I tried that but no dice. Also tried to run the calculation only if this.state.data exists within componentDidMount but no luck. What I get now is just the number 0 were the StatCard would be – LoF10 Nov 15 '19 at 20:43
  • 1
    I see, it's because you don't understand how promises/async behavior works. You need to `await requestData()` instead of adding it to the requestData function directly and add `async fetchData = () => {}`. Better yet, `fetchData` should only fetch the data and store it in state. Then you should use `componentDidUpdate` to trigger `calculateInspections()` only when `this.state.data` changes – Chase DeAnda Nov 15 '19 at 21:05
  • Hey Chase, trying to operationalize what you pointed out, albeit I admit I'm still learning so not sure how exactly to implement. i added my attempt to my question if you could take a look and point me in the right direction? – LoF10 Nov 15 '19 at 21:33
0

First of all, I don't think you need a separate function calculateInspections(). You can put that logic in the then callback.

fetchData = () => {
  fetch(`https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=latitude > 39 AND latitude< 45 AND inspection_date >= '${this.state.dateInput}'&$limit=50000`)
    .then(res => res.json())
    .then(data => {
      this.setState({
        data: data,
        loading: true,
        totalInspections: this.state.data.length
      })
    })
}

Secondly, setting this.state.totalInspections is effectively redundant, since you can simple do:

{this.state.data && <StatCard totalInspections={this.state.data.length} /> }

Lastly, avoid using componentDidUpdate() hook when you're new to react. Most of the time you end up shooting yourself in the foot.

Currently your Attempt Repair just got you into an infinite render loop. This happens because whenever you call setState(), it'll call componentDidUpdate() lifecycle hook after rendering. But within componentDidUpdate() you call again setState(), which induces a follow-up call to the same lifecycle hook, and thus the loop goes on and on.

If you must use componentDidUpdate() and call setState() inside, rule of thumbs, always put a stop-condition ahead of it. In you case, it'll be:

componentDidUpdate () {
  if (this.state.data) {
    if (this.state.totalInspections !== this.state.data.length) {
      this.setState({ totalInspections: this.state.data.length })
    }
  }
}
hackape
  • 18,643
  • 2
  • 29
  • 57
0

Here is my solution.

class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            data: [],
            dateInput: '2019-10-01',
            loading: false,
            error: false
        };
    }

    async componentDidMount() {
        try {
            await this.fetchData(this.state.dateInput);
        } catch (err) {
            this.setState({ loading: false, error: true });
        }
    }

    fetchData = (date) => new Promise(resolve => {
        this.setState({ loading: true });
        fetch(`https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=latitude > 39 AND latitude< 45 AND inspection_date >= '${date}'&$limit=50000`)
            .then(res => res.json())
            .then(res => {
                this.setState({ data: res, loading: false, error: false });
                resolve(res.data);
            });
    })

    handleDateInput = e => {
        this.setState({ dateInput: e.target.value }) //update state with the new date value
        this.fetchData(e.target.value);
    }

    render() {
        const { loading, data } = this.state;
        return (
            <div>
                {loading && (
                    <div className={classes.splash_screen}>
                        <div className={classes.loader}></div>
                    </div>
                )}
                {data && <StatCard totalInspections={data.length} />}
                <Dates handleDateInput={this.handleDateInput} />
                <Leaf data={data} />
            </div>
        );
    }
}
TopW3
  • 1,477
  • 1
  • 8
  • 14
0

There are two ways of achieving this:

  1. You can put calculator in componentDidUpdate() and write a condition to just calculate once

componentDidUpdate(prevProps, prevState) {
    const data = this.state.data;

    // this line check if we have data or we have new data,
    // calculate length once
    if (data.length || !isEqual(data, prevState.data)) {
        calculateInspections()
    }
}

// isEqual() is a lodash function to compare two object or array
  1. You can stop your rendering until data is fetched

async componentDidMount() {
      await fetchData()
    }

fetchData = () => {
    const requestData = async() => {
    await fetch(`https://data.cityofnewyork.us/resource/p937-wjvj.json?$where=latitude > 39 AND latitude< 45 AND inspection_date >= '${this.state.dateInput}'&$limit=50000`)
        .then(res => res.json())
        .then(res =>
        //console.log(res)
        this.setState({
            data: res,
            loading: true,
            totalInspections: res.length
        })
        )
    }
    // in above situation you just setState when you are sure
    // that data has come

    //call the function
    requestData();
}
codesnerd
  • 767
  • 2
  • 8
  • 23