1

I've read many posts but this is my first question! Let me first thank everyone for their support, and offer a description of the problem before a lengthy block of code! Any help is appreciated...I think I'm close!

QUESTION/PROBLEM: my setState method only works on the initial data fetch. Subsequent attempts will fetch the new data, but don't reset the state, and therefore the child component does not receive new props!

WHY/WHAT: I'm using papa parse to fetch csv data and convert it to JSON, and then pass this data to the child. I am using a D3 map library called react-simple-maps, and then calling animations with GSAP/Greensock.

Papa parse defaults to a callback instead of a promise (although promises are possible), so I've created an _.isMounted method that I can toggle...reason for this was that once I fetched data async, my animations would no longer run in the child/map component as they are called in componentDidMount before the fetch was complete! now my fix is working, but won't work over and over again! I will be passing new data to create sets of map markers several times and hour! See my commented code below!

class MapContainerParent extends Component {
  _isMounted = false; //toggling this allows the child component to wait for data fetch to complete before render
  _isRefreshed = false; //tried, but not working?
  constructor(props) {
    super(props);
    this.state = {
      data: [],
      isLoading: true,
      error: null
    };
    this.updateData = this.updateData.bind(this);
  }

  //I'm using papaparse to fetch and convert the csv file to JSON
//it uses a callback instead of a promise, which may be the issue?

  fetchAndConvert() {
    //Papa Parse to convert CSV to JSON...also fetches from S3 bucket
    const Papa = require("papaparse");
    const dataFilePath =
      "url of where the csv file lives";
    Papa.parse(dataFilePath, {
      download: true,
      header: false,
      skipEmptyLines: true,
      complete: this.updateData //callback, which sets the state after the data is in proper format
    });
  }
  //HEre is the callback
  updateData(result) {
    const data = result.data;
    const mapData = data.map(item => {.....
    //takes the resulting json and formats it

    console.log("MapData restructured:", mapData);
    if (this._isMounted) { //I THINK THE PROBLEM IS HERE!
      this.setState({ //Only sets the state on initial render! 
      //makes sense since I have told it to setState when isMounted is true. 
      //Since I'm fetching new data several times an hour, this won't do! 
      //How can I add another condition to reset state when new data is present?
        data: mapData,
        isLoading: false,
        error: false
      });
    } //tried to add an _isRefreshed condition, not working? 
    else if (this._isMounted && this._isRefreshed === false) {
      this.setState({
        data: mapData,
        isLoading: false,
        error: false
      });
    }
  }
  componentDidMount() {
    this._isMounted = true;
    this.fetchAndConvert();
    this._isRefreshed = true; //dn work
    setInterval(this.fetchAndConvert, 30000);
    //runs the function at an interval I set
    //I see the new data when I preview the network request
    //but it does not trigger a new setState???
  }

  componentWillUnmount() {
    this._isMounted = false;
    this._isRefreshed = false; //not working...
  }

  renderLoading() {
    return <div > loading... < /div>;
  }
  renderError() {
    return <div > Error... < /div>;
  }
  renderMap() {
    return ( 
        <div>
           <MapChild data = {this.state.data/> <
         </div>
    );
  }
  render() {
    if (this.state.loading) {
      return this.renderLoading();
    } else if (this.state.data.length > 0) {
      return this.renderMap();
    } else {
      return this.renderError();
    }
  }
}
export default MapContainerParent;
Union In Design
  • 141
  • 1
  • 6
  • Did my answer work for you? Consider [accepting it](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work#answer-5235) if that's the case. – Tholle Mar 05 '19 at 19:52

2 Answers2

0

this will not be what you expect when the interval function is invoked.

Try binding fetchAndConvert to this in the constructor like you do with updateData and it will work as expected.

constructor(props) {
  super(props);
  this.state = {
    data: [],
    isLoading: true,
    error: null
  };
  this.updateData = this.updateData.bind(this);
  this.fetchAndConvert = this.fetchAndConvert.bind(this);
}
Tholle
  • 108,070
  • 19
  • 198
  • 189
  • 1
    Thanks! This worked - missed that...or I guess I could have used a declaration instead of an expression for fetchAndConvert! Much appreciated – Union In Design Feb 27 '19 at 20:29
  • @UnionInDesign You're welcome! Yes, there are many valid ways to do it. Consider [accepting the answer](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work#answer-5235) if you feel it answered your question. – Tholle Feb 27 '19 at 20:49
  • Follow up question @Tholle - while this did fix my issue of the state never being reset in the parent component, I'm back to the same error I was getting before where the child component will call the animations before the new props are present, causing an undefined (GSAP says it cannot tween a null target). I still believe that this is state is only set when _.isMounted is true...but if the component has already mounted, what can we do on subsequent attempts to fetch new data? – Union In Design Feb 27 '19 at 23:27
  • @UnionInDesign That's a very different question. Consider opening up a new question and someone will be able to help you. – Tholle Feb 27 '19 at 23:29
0

So...there is a problem with using _.isMounted to call a setState. According to this blog post from React it is anti-pattern:

https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html

While I based my use of this method on the following blog post, with some very good ongoing dialogue about this issue in the comments, I still haven't found what I consider to be a long-term solution. https://www.robinwieruch.de/react-warning-cant-call-setstate-on-an-unmounted-component/

What works, for now, is inside the child component...call animation in componentDidMount, and use componentDidUpdate to do a deep comparison of the data/props (I used lodash _.isEqual), and if it gets new data, kill the previous animation and load it again. While this is running, a 2 second loading animation buys some time for the data to come in. I don't want to share this code because I would also call it "anti-pattern" or just bad practice...but it works 100% of the time! When I find a better way I''' update the thread!

Union In Design
  • 141
  • 1
  • 6