LoF10
LoF10

Reputation: 2127

How to properly time data rendering in react?

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

Upvotes: 11

Views: 3146

Answers (5)

ehsan parsania
ehsan parsania

Reputation: 92

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

Upvotes: 0

Daniel Duong
Daniel Duong

Reputation: 1104

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

Upvotes: 1

TopW3
TopW3

Reputation: 1527

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

Upvotes: 0

hackape
hackape

Reputation: 19977

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

Upvotes: 0

Chase DeAnda
Chase DeAnda

Reputation: 16441

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

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

Upvotes: 0

Related Questions